Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions apis/projectcontour/v1alpha1/contourconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,16 @@ type EnvoyConfig struct {
// +optional
Service *NamespacedName `json:"service,omitempty"`

// LoadBalancerStatus specifies the source for load balancer status addresses
// that Contour will set on HTTPProxy, Ingress, and Gateway resources.
//
// Exactly one of the fields must be set.
//
// If spec.ingress.statusAddress is set, it takes precedence over this field.
// If this field is empty, spec.envoy.service is used as default.
// +optional
LoadBalancerStatus *LoadBalancerStatusConfig `json:"loadBalancerStatus,omitempty"`

// Defines the HTTP Listener for Envoy.
//
// Contour's default is { address: "0.0.0.0", port: 8080, accessLog: "/dev/stdout" }.
Expand Down Expand Up @@ -310,6 +320,23 @@ type EnvoyConfig struct {
OMEnforcedHealth *HealthConfig `json:"omEnforcedHealth,omitempty"`
}

// LoadBalancerStatusConfig defines the source for load balancer status addresses.
// Exactly one of the fields must be set.
// +kubebuilder:validation:XValidation:message="exactly one of service, ingress, or addresses must be set",rule="[has(self.service), has(self.ingress), has(self.addresses)].exists_one(x, x)"
type LoadBalancerStatusConfig struct {
// Service watches the named Service's status.loadBalancer for addresses.
// +optional
Service *NamespacedName `json:"service,omitempty"`

// Ingress watches the named Ingress's status.loadBalancer for addresses.
// +optional
Ingress *NamespacedName `json:"ingress,omitempty"`

// Addresses specifies static address(es) to use directly (IP or FQDN).
// +optional
Addresses []string `json:"addresses,omitempty"`
}

// DebugConfig contains Contour specific troubleshooting options.
type DebugConfig struct {
// Defines the Contour debug address interface.
Expand Down
35 changes: 35 additions & 0 deletions apis/projectcontour/v1alpha1/zz_generated.deepcopy.go

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

7 changes: 7 additions & 0 deletions changelogs/unreleased/7595-tsaarni-minor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
## Load balancer status source from Ingress objects

Contour can now watch an Ingress object's LoadBalancer status as the source for address propagation to `Ingress` and `HTTPProxy` status.
A new `--load-balancer-status` flag unifies address source configuration with support for `address:`, `service:`, and `ingress:` source kinds.
The same setting is available as `spec.envoy.loadBalancer` in the ContourConfiguration CRD or `load-balancer-status` in the config file.

(@hligit, @kahirokunn and @tsaarni)
26 changes: 24 additions & 2 deletions cmd/contour/ingressstatus.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,20 +59,22 @@ type loadBalancerStatusWriter struct {
statusAddress string
serviceName string
serviceNamespace string
ingressName string
ingressNamespace string
}

func (isw *loadBalancerStatusWriter) NeedLeaderElection() bool {
return true
}

func (isw *loadBalancerStatusWriter) Start(ctx context.Context) error {
// Register an informer to watch envoy's service if we haven't been given static details.
// Register an informer to watch envoy's service or ingress if we haven't been given static details.
// The informer is registered only after leader election to prevent events from being sent before the status writer
// is ready to process them.
if lbAddress := isw.statusAddress; len(lbAddress) > 0 {
isw.log.WithField("loadbalancer-address", lbAddress).Info("Using supplied information for Ingress status")
isw.lbStatus <- parseStatusFlag(lbAddress)
} else {
} else if isw.serviceName != "" {
// Register Service informer to watch for status updates.
var serviceHandler client_go_cache.ResourceEventHandler = &k8s.ServiceStatusLoadBalancerWatcher{
ServiceName: isw.serviceName,
Expand All @@ -92,6 +94,26 @@ func (isw *loadBalancerStatusWriter) Start(ctx context.Context) error {
isw.log.WithError(err).Fatal("failed to add Service event handler")
return err
}
} else if isw.ingressName != "" {
// Register Ingress informer to watch for status updates.
var ingressHandler client_go_cache.ResourceEventHandler = &k8s.IngressStatusLoadBalancerWatcher{
IngressName: isw.ingressName,
LBStatus: isw.lbStatus,
Log: isw.log.WithField("context", "ingressStatusLoadBalancerWatcher"),
}
if isw.ingressNamespace != "" {
ingressHandler = k8s.NewNamespaceFilter([]string{isw.ingressNamespace}, ingressHandler)
}
inf, err := isw.cache.GetInformer(ctx, &networking_v1.Ingress{})
if err != nil {
isw.log.WithError(err).Fatal("failed to get Ingress informer")
return err
}
_, err = inf.AddEventHandler(ingressHandler)
if err != nil {
isw.log.WithError(err).Fatal("failed to add Ingress event handler")
return err
}
}

u := &k8s.StatusAddressUpdater{
Expand Down
59 changes: 46 additions & 13 deletions cmd/contour/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"net/http"
"os"
"strconv"
"strings"
"time"

"github.com/alecthomas/kingpin/v2"
Expand Down Expand Up @@ -169,6 +170,8 @@ func registerServe(app *kingpin.Application) (*kingpin.CmdClause, *serveContext)
serve.Flag("leader-election-resource-namespace", "The namespace of the resource (Lease) leader election will lease.").Default(config.GetenvOr("CONTOUR_NAMESPACE", "projectcontour")).StringVar(&ctx.LeaderElection.Namespace)
serve.Flag("leader-election-retry-period", "The interval which Contour will attempt to acquire leadership lease.").Default("2s").DurationVar(&ctx.LeaderElection.RetryPeriod)

serve.Flag("load-balancer-status", "Address to set or the source to inspect for load balancer status.").PlaceHolder("<kind:namespace/name|address>").StringVar(&ctx.loadBalancerStatus)

serve.Flag("root-namespaces", "Restrict contour to searching these namespaces for root ingress routes.").PlaceHolder("<ns,ns>").StringVar(&ctx.rootNamespaces)

serve.Flag("stats-address", "Envoy /stats interface address.").PlaceHolder("<ipaddr>").StringVar(&ctx.statsAddr)
Expand Down Expand Up @@ -675,19 +678,8 @@ func (s *Server) doServe() error {
return err
}

// Set up ingress load balancer status writer.
lbsw := &loadBalancerStatusWriter{
log: s.log.WithField("context", "loadBalancerStatusWriter"),
cache: s.mgr.GetCache(),
lbStatus: make(chan core_v1.LoadBalancerStatus, 1),
ingressClassNames: ingressClassNames,
gatewayRef: gatewayRef,
statusUpdater: sh.Writer(),
statusAddress: contourConfiguration.Ingress.StatusAddress,
serviceName: contourConfiguration.Envoy.Service.Name,
serviceNamespace: contourConfiguration.Envoy.Service.Namespace,
}
if err := s.mgr.Add(lbsw); err != nil {
// Set up load balancer status writer.
if err := s.setupLoadBalancerStatusWriter(contourConfiguration, ingressClassNames, gatewayRef, sh.Writer()); err != nil {
return err
}

Expand All @@ -714,6 +706,47 @@ func (s *Server) doServe() error {
return s.mgr.Start(signals.SetupSignalHandler())
}

func (s *Server) setupLoadBalancerStatusWriter(
contourConfiguration contour_v1alpha1.ContourConfigurationSpec,
ingressClassNames []string,
gatewayRef *types.NamespacedName,
statusUpdater k8s.StatusUpdater,
) error {
lbsw := &loadBalancerStatusWriter{
log: s.log.WithField("context", "loadBalancerStatusWriter"),
cache: s.mgr.GetCache(),
lbStatus: make(chan core_v1.LoadBalancerStatus, 1),
ingressClassNames: ingressClassNames,
gatewayRef: gatewayRef,
statusUpdater: statusUpdater,
}
// Resolve the load balancer status source from configuration.
// Priority:
// 1. spec.ingress.statusAddress
// 2. spec.envoy.loadBalancerStatus
// 3. spec.envoy.service

if lbAddress := contourConfiguration.Ingress.StatusAddress; len(lbAddress) > 0 {
lbsw.statusAddress = lbAddress
} else if lb := contourConfiguration.Envoy.LoadBalancerStatus; lb != nil {
switch {
case lb.Service != nil:
lbsw.serviceName = lb.Service.Name
lbsw.serviceNamespace = lb.Service.Namespace
case lb.Ingress != nil:
lbsw.ingressName = lb.Ingress.Name
lbsw.ingressNamespace = lb.Ingress.Namespace
case len(lb.Addresses) > 0:
lbsw.statusAddress = strings.Join(lb.Addresses, ",")
}
} else {
lbsw.serviceName = contourConfiguration.Envoy.Service.Name
lbsw.serviceNamespace = contourConfiguration.Envoy.Service.Namespace
}

return s.mgr.Add(lbsw)
}

func (s *Server) getExtensionSvcConfig(name, namespace string) (xdscache_v3.ExtensionServiceConfig, error) {
extensionSvc := &contour_v1alpha1.ExtensionService{}
key := client.ObjectKey{
Expand Down
61 changes: 61 additions & 0 deletions cmd/contour/serve_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -307,3 +307,64 @@
require.FailNow(t, "IngressProcessor not found in list of DAG builder's processors")
return nil
}

func TestParseLoadBalancerStatusSource(t *testing.T) {
tests := []struct {
name string
input string
wantKind string
wantAddress string
wantNN types.NamespacedName
}{
{
name: "service",
input: "service:namespace-1/name-1",
wantKind: "service",
wantNN: types.NamespacedName{Namespace: "namespace-1", Name: "name-1"},
},
{
name: "ingress",
input: "ingress:namespace-1/name-1",
wantKind: "ingress",
wantNN: types.NamespacedName{Namespace: "namespace-1", Name: "name-1"},
},
{
name: "address with hostname",
input: "address:example.com",
wantKind: "address",
wantAddress: "example.com",
},
{
name: "address with IP",
input: "address:1.2.3.4",
wantKind: "address",
wantAddress: "1.2.3.4",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
kind, address, nn, err := parseLoadBalancerStatusSource(tt.input)

Check failure on line 346 in cmd/contour/serve_test.go

View workflow job for this annotation

GitHub Actions / lint

undefined: parseLoadBalancerStatusSource

Check failure on line 346 in cmd/contour/serve_test.go

View workflow job for this annotation

GitHub Actions / CodeQL-Build

undefined: parseLoadBalancerStatusSource
require.NoError(t, err)
assert.Equal(t, tt.wantKind, kind)
assert.Equal(t, tt.wantAddress, address)
assert.Equal(t, tt.wantNN, nn)
})
}

errors := []struct {
name string
input string
}{
{name: "empty string", input: ""},
{name: "empty kind", input: ":value"},
{name: "empty value", input: "service:"},
{name: "no colon", input: "service"},
{name: "unsupported kind", input: "unknown:ns/name"},
}
for _, tt := range errors {
t.Run(tt.name, func(t *testing.T) {
_, _, _, err := parseLoadBalancerStatusSource(tt.input)

Check failure on line 366 in cmd/contour/serve_test.go

View workflow job for this annotation

GitHub Actions / lint

undefined: parseLoadBalancerStatusSource (typecheck)

Check failure on line 366 in cmd/contour/serve_test.go

View workflow job for this annotation

GitHub Actions / CodeQL-Build

undefined: parseLoadBalancerStatusSource
assert.Error(t, err)
})
}
}
68 changes: 67 additions & 1 deletion cmd/contour/servecontext.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ type serveContext struct {
// PermitInsecureGRPC disables TLS on Contour's gRPC listener.
PermitInsecureGRPC bool

// loadBalancerStatus is the CLI flag value for --load-balancer-status, before parsing into a structured format in loadBalancerStatusConfig.
loadBalancerStatus string

// Leader election configuration.
LeaderElection LeaderElection

Expand Down Expand Up @@ -629,7 +632,8 @@ func (ctx *serveContext) convertToContourConfigurationSpec() contour_v1alpha1.Co
EnvoyAdminPort: &ctx.Config.Network.EnvoyAdminPort,
EnvoyStripTrailingHostDot: &ctx.Config.Network.EnvoyStripTrailingHostDot,
},
OMEnforcedHealth: envoyOMEnforcedHealthListenerConfig,
OMEnforcedHealth: envoyOMEnforcedHealthListenerConfig,
LoadBalancerStatus: ctx.loadBalancerStatusConfig(),
},
Gateway: gatewayConfig,
HTTPProxy: &contour_v1alpha1.HTTPProxyConfig{
Expand Down Expand Up @@ -677,3 +681,65 @@ func setMetricsFromConfig(src config.MetricsServerParameters, dst *contour_v1alp
}
}
}

// loadBalancerStatusConfig returns the LoadBalancerStatusConfig, preferring
// the CLI flag over the config file struct.
func (ctx *serveContext) loadBalancerStatusConfig() *contour_v1alpha1.LoadBalancerStatusConfig {
// CLI flag takes precedence over config file.
if ctx.loadBalancerStatus != "" {
parts := strings.SplitN(ctx.loadBalancerStatus, ":", 2)
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
return nil
}

switch strings.ToLower(parts[0]) {
case "service":
nn := k8s.NamespacedNameFrom(parts[1])
return &contour_v1alpha1.LoadBalancerStatusConfig{
Service: &contour_v1alpha1.NamespacedName{
Namespace: nn.Namespace,
Name: nn.Name,
},
}
case "ingress":
nn := k8s.NamespacedNameFrom(parts[1])
return &contour_v1alpha1.LoadBalancerStatusConfig{
Ingress: &contour_v1alpha1.NamespacedName{
Namespace: nn.Namespace,
Name: nn.Name,
},
}
case "address":
return &contour_v1alpha1.LoadBalancerStatusConfig{
Addresses: strings.Split(parts[1], ","),
}
default:
return nil
}
}

// Fallback to config file.
src := ctx.Config.LoadBalancerStatus
switch {
case src.Service != nil:
return &contour_v1alpha1.LoadBalancerStatusConfig{
Service: &contour_v1alpha1.NamespacedName{
Namespace: src.Service.Namespace,
Name: src.Service.Name,
},
}
case src.Ingress != nil:
return &contour_v1alpha1.LoadBalancerStatusConfig{
Ingress: &contour_v1alpha1.NamespacedName{
Namespace: src.Ingress.Namespace,
Name: src.Ingress.Name,
},
}
case len(src.Addresses) > 0:
return &contour_v1alpha1.LoadBalancerStatusConfig{
Addresses: src.Addresses,
}
default:
return nil
}
}
Loading
Loading