Skip to content

Inspect loadbalancer address from Envoy ingress objects #5987

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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
10 changes: 10 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"`

// LoadBalancer specifies how Contour should set the ingress status address.
// If provided, the value can be in one of the formats:
// - address:<address,...>: Contour will use the provided comma separated list of addresses directly. The address can be a fully qualified domain name or an IP address.
// - service:<namespace>/<name>: Contour will use the address of the designated service.
// - ingress:<namespace>/<name>: Contour will use the address of the designated ingress.
//
// Contour's default is an empty string.
// +optional
LoadBalancer string `json:"loadBalancer,omitempty"`

// Defines the HTTP Listener for Envoy.
//
// Contour's default is { address: "0.0.0.0", port: 8080, accessLog: "/dev/stdout" }.
Expand Down
146 changes: 134 additions & 12 deletions cmd/contour/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"net/http"
"os"
"strconv"
"strings"
"time"

"github.com/alecthomas/kingpin/v2"
Expand Down Expand Up @@ -168,6 +169,8 @@
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 ingress status.").PlaceHolder("<kind:namespace/name|address>").StringVar(&ctx.Config.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 @@ -673,18 +676,7 @@
}

// 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 {
if err := s.setupIngressLoadBalancerStatusWriter(contourConfiguration, ingressClassNames, gatewayRef, sh.Writer()); err != nil {

Check warning on line 679 in cmd/contour/serve.go

View check run for this annotation

Codecov / codecov/patch

cmd/contour/serve.go#L679

Added line #L679 was not covered by tests
return err
}

Expand All @@ -711,6 +703,136 @@
return s.mgr.Start(signals.SetupSignalHandler())
}

func (s *Server) setupIngressLoadBalancerStatusWriter(
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,
statusAddress: contourConfiguration.Ingress.StatusAddress,
serviceName: contourConfiguration.Envoy.Service.Name,
serviceNamespace: contourConfiguration.Envoy.Service.Namespace,
}
if err := s.mgr.Add(lbsw); err != nil {
return err
}

Check warning on line 725 in cmd/contour/serve.go

View check run for this annotation

Codecov / codecov/patch

cmd/contour/serve.go#L711-L725

Added lines #L711 - L725 were not covered by tests

elbs := &envoyLoadBalancerStatus{}
if lbAddress := contourConfiguration.Ingress.StatusAddress; len(lbAddress) > 0 {
elbs.Kind = "hostname"
elbs.FQDNs = lbAddress
} else if contourConfiguration.Envoy.LoadBalancer != "" {
status, err := parseEnvoyLoadBalancerStatus(contourConfiguration.Envoy.LoadBalancer)
if err != nil {
return err
}
elbs = status
} else {
elbs.Kind = "service"
elbs.Namespace = contourConfiguration.Envoy.Service.Namespace
elbs.Name = contourConfiguration.Envoy.Service.Name
}
switch strings.ToLower(elbs.Kind) {
case "hostname":
s.log.WithField("loadbalancer-fqdns", lbAddress).Info("Using supplied hostname for Ingress status")
lbsw.lbStatus <- parseStatusFlag(elbs.FQDNs)
case "service":
// Register an informer to watch supplied service
serviceHandler := &k8s.ServiceStatusLoadBalancerWatcher{
ServiceName: elbs.Name,
LBStatus: lbsw.lbStatus,
Log: s.log.WithField("context", "serviceStatusLoadBalancerWatcher"),
}

var handler cache.ResourceEventHandler = serviceHandler
if elbs.Namespace != "" {
handler = k8s.NewNamespaceFilter([]string{elbs.Namespace}, handler)
}

Check warning on line 757 in cmd/contour/serve.go

View check run for this annotation

Codecov / codecov/patch

cmd/contour/serve.go#L727-L757

Added lines #L727 - L757 were not covered by tests

if err := s.informOnResource(&core_v1.Service{}, handler); err != nil {
s.log.WithError(err).WithField("resource", "services").Fatal("failed to create services informer")
}
s.log.Infof("Watching %s for Ingress status", elbs)
case "ingress":
// Register an informer to watch supplied ingress
ingressHandler := &k8s.IngressStatusLoadBalancerWatcher{
IngressName: elbs.Name,
LBStatus: lbsw.lbStatus,
Log: s.log.WithField("context", "ingressStatusLoadBalancerWatcher"),
}

var handler cache.ResourceEventHandler = ingressHandler
if elbs.Namespace != "" {
handler = k8s.NewNamespaceFilter([]string{elbs.Namespace}, handler)
}

Check warning on line 774 in cmd/contour/serve.go

View check run for this annotation

Codecov / codecov/patch

cmd/contour/serve.go#L759-L774

Added lines #L759 - L774 were not covered by tests

if err := s.informOnResource(&networking_v1.Ingress{}, handler); err != nil {
s.log.WithError(err).WithField("resource", "ingresses").Fatal("failed to create ingresses informer")
}
s.log.Infof("Watching %s for Ingress status", elbs)
default:
return fmt.Errorf("unsupported ingress kind: %s", elbs.Kind)

Check warning on line 781 in cmd/contour/serve.go

View check run for this annotation

Codecov / codecov/patch

cmd/contour/serve.go#L776-L781

Added lines #L776 - L781 were not covered by tests
}

return nil

Check warning on line 784 in cmd/contour/serve.go

View check run for this annotation

Codecov / codecov/patch

cmd/contour/serve.go#L784

Added line #L784 was not covered by tests
}

type envoyLoadBalancerStatus struct {
Kind string
FQDNs string
config.NamespacedName
}

func (elbs *envoyLoadBalancerStatus) String() string {
if elbs.Kind == "hostname" {
return fmt.Sprintf("%s:%s", elbs.Kind, elbs.FQDNs)
}
return fmt.Sprintf("%s:%s/%s", elbs.Kind, elbs.Namespace, elbs.Name)

Check warning on line 797 in cmd/contour/serve.go

View check run for this annotation

Codecov / codecov/patch

cmd/contour/serve.go#L793-L797

Added lines #L793 - L797 were not covered by tests
}

func parseEnvoyLoadBalancerStatus(s string) (*envoyLoadBalancerStatus, error) {
parts := strings.SplitN(s, ":", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("invalid load-balancer-status: %s", s)
}

if parts[1] == "" {
return nil, fmt.Errorf("invalid load-balancer-status: empty object reference")
}

elbs := envoyLoadBalancerStatus{}

elbs.Kind = strings.ToLower(parts[0])
switch elbs.Kind {
case "ingress", "service":
parts = strings.Split(parts[1], "/")
if len(parts) != 2 {
return nil, fmt.Errorf("invalid load-balancer-status: %s is not in the format of <namespace>/<name>", s)
}

if parts[0] == "" || parts[1] == "" {
return nil, fmt.Errorf("invalid load-balancer-status: <namespace> or <name> is empty")
}
elbs.Namespace = parts[0]
elbs.Name = parts[1]
case "hostname":
elbs.FQDNs = parts[1]
case "":
return nil, fmt.Errorf("invalid load-balancer-status: kind is empty")
default:
return nil, fmt.Errorf("invalid load-balancer-status: unsupported kind: %s", elbs.Kind)
}

return &elbs, nil
}

func (s *Server) getExtensionSvcConfig(name, namespace string) (xdscache_v3.ExtensionServiceConfig, error) {
extensionSvc := &contour_v1alpha1.ExtensionService{}
key := client.ObjectKey{
Expand Down
106 changes: 106 additions & 0 deletions cmd/contour/serve_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (

contour_v1alpha1 "github.com/projectcontour/contour/apis/projectcontour/v1alpha1"
"github.com/projectcontour/contour/internal/dag"
"github.com/projectcontour/contour/pkg/config"
)

func TestGetDAGBuilder(t *testing.T) {
Expand Down Expand Up @@ -256,3 +257,108 @@ func mustGetIngressProcessor(t *testing.T, builder *dag.Builder) *dag.IngressPro
require.FailNow(t, "IngressProcessor not found in list of DAG builder's processors")
return nil
}

func TestParseEnvoyLoadBalancerStatus(t *testing.T) {
tests := []struct {
name string
status string
want envoyLoadBalancerStatus
}{
{
name: "Service",
status: "service:namespace-1/name-1",
want: envoyLoadBalancerStatus{
Kind: "service",
NamespacedName: config.NamespacedName{
Name: "name-1",
Namespace: "namespace-1",
},
},
},
{
name: "Ingress",
status: "ingress:namespace-1/name-1",
want: envoyLoadBalancerStatus{
Kind: "ingress",
NamespacedName: config.NamespacedName{
Name: "name-1",
Namespace: "namespace-1",
},
},
},
{
name: "hostname",
status: "hostname:example.com",
want: envoyLoadBalancerStatus{
Kind: "hostname",
FQDNs: "example.com",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r, err := parseEnvoyLoadBalancerStatus(tt.status)
require.NoError(t, err)
assert.Equal(t, tt.want, *r)
})
}

tests2 := []struct {
name string
status string
error string
}{
{
name: "Empty",
status: "",
error: "invalid",
},
{
name: "No kind",
status: ":n",
error: "kind is empty",
},
{
name: "Invalid kind",
status: "test:n",
error: "unsupported kind",
},
{
name: "No reference",
status: "service:",
error: "empty object reference",
},
{
name: "No colon",
status: "service",
error: "invalid",
},
{
name: "No slash",
status: "service:name-1",
error: "not in the format",
},
{
name: "starts with slash",
status: "service:/name-1",
error: "is empty",
},
{
name: "ends with slash",
status: "service:name-1/",
error: "is empty",
},
{
name: "two many slashes",
status: "service:name/x/y",
error: "not in the format",
},
}
for _, tt := range tests2 {
t.Run(tt.name, func(t *testing.T) {
_, err := parseEnvoyLoadBalancerStatus(tt.status)
require.Error(t, err)
assert.Contains(t, err.Error(), tt.error)
})
}
}
1 change: 1 addition & 0 deletions cmd/contour/servecontext.go
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,7 @@ func (ctx *serveContext) convertToContourConfigurationSpec() contour_v1alpha1.Co
EnvoyStripTrailingHostDot: &ctx.Config.Network.EnvoyStripTrailingHostDot,
},
OMEnforcedHealth: envoyOMEnforcedHealthListenerConfig,
LoadBalancer: ctx.Config.LoadBalancerStatus,
},
Gateway: gatewayConfig,
HTTPProxy: &contour_v1alpha1.HTTPProxyConfig{
Expand Down
18 changes: 18 additions & 0 deletions examples/contour/01-crds.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,15 @@ spec:
Contour's default is false.
type: boolean
type: object
loadBalancer:
description: |-
LoadBalancer specifies how Contour should set the ingress status address.
If provided, the value can be in one of the formats:
- address:<address,...>: Contour will use the provided comma separated list of addresses directly. The address can be a fully qualified domain name or an IP address.
- service:<namespace>/<name>: Contour will use the address of the designated service.
- ingress:<namespace>/<name>: Contour will use the address of the designated ingress.
Contour's default is an empty string.
type: string
logging:
description: Logging defines how Envoy's logs can be configured.
properties:
Expand Down Expand Up @@ -4323,6 +4332,15 @@ spec:
Contour's default is false.
type: boolean
type: object
loadBalancer:
description: |-
LoadBalancer specifies how Contour should set the ingress status address.
If provided, the value can be in one of the formats:
- address:<address,...>: Contour will use the provided comma separated list of addresses directly. The address can be a fully qualified domain name or an IP address.
- service:<namespace>/<name>: Contour will use the address of the designated service.
- ingress:<namespace>/<name>: Contour will use the address of the designated ingress.
Contour's default is an empty string.
type: string
logging:
description: Logging defines how Envoy's logs can be configured.
properties:
Expand Down
18 changes: 18 additions & 0 deletions examples/render/contour-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,15 @@ spec:
Contour's default is false.
type: boolean
type: object
loadBalancer:
description: |-
LoadBalancer specifies how Contour should set the ingress status address.
If provided, the value can be in one of the formats:
- address:<address,...>: Contour will use the provided comma separated list of addresses directly. The address can be a fully qualified domain name or an IP address.
- service:<namespace>/<name>: Contour will use the address of the designated service.
- ingress:<namespace>/<name>: Contour will use the address of the designated ingress.
Contour's default is an empty string.
type: string
logging:
description: Logging defines how Envoy's logs can be configured.
properties:
Expand Down Expand Up @@ -4542,6 +4551,15 @@ spec:
Contour's default is false.
type: boolean
type: object
loadBalancer:
description: |-
LoadBalancer specifies how Contour should set the ingress status address.
If provided, the value can be in one of the formats:
- address:<address,...>: Contour will use the provided comma separated list of addresses directly. The address can be a fully qualified domain name or an IP address.
- service:<namespace>/<name>: Contour will use the address of the designated service.
- ingress:<namespace>/<name>: Contour will use the address of the designated ingress.
Contour's default is an empty string.
type: string
logging:
description: Logging defines how Envoy's logs can be configured.
properties:
Expand Down
Loading
Loading