Skip to content

Commit 9640ce1

Browse files
committed
Add a new load-balancer-status flag for setting ingress details
Signed-off-by: Haitao Li <[email protected]>
1 parent 7380ee8 commit 9640ce1

File tree

14 files changed

+422
-110
lines changed

14 files changed

+422
-110
lines changed

apis/projectcontour/v1alpha1/contourconfig.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -253,11 +253,15 @@ type EnvoyConfig struct {
253253
// +optional
254254
Service *NamespacedName `json:"service,omitempty"`
255255

256-
// Ingress holds Envoy service parameters for setting Ingress status.
256+
// LoadBalancer specifies how Contour should set the ingress status address.
257+
// If provided, the value can be in one of the formats:
258+
// - hostname:<address,...>: Contour will use the provided comma separated list of addresses directly.
259+
// - service:<namespace>/<name>: Contour will use the address of the designated service.
260+
// - ingress:<namespace>/<name>: Contour will use the address of the designated ingress.
257261
//
258-
// Contour's default is { namespace: "projectcontour", name: "envoy" }.
262+
// Contour's default is an empty string.
259263
// +optional
260-
Ingress *NamespacedName `json:"ingress,omitempty"`
264+
LoadBalancer string `json:"loadBalancer,omitempty"`
261265

262266
// Defines the HTTP Listener for Envoy.
263267
//

cmd/contour/serve.go

Lines changed: 117 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"net/http"
2121
"os"
2222
"strconv"
23+
"strings"
2324
"time"
2425

2526
"github.com/alecthomas/kingpin/v2"
@@ -145,8 +146,6 @@ func registerServe(app *kingpin.Application) (*kingpin.CmdClause, *serveContext)
145146
serve.Flag("envoy-service-https-port", "Kubernetes Service port for HTTPS requests.").PlaceHolder("<port>").IntVar(&ctx.httpsPort)
146147
serve.Flag("envoy-service-name", "Name of the Envoy service to inspect for Ingress status details.").PlaceHolder("<name>").StringVar(&ctx.Config.EnvoyServiceName)
147148
serve.Flag("envoy-service-namespace", "Envoy Service Namespace.").PlaceHolder("<namespace>").StringVar(&ctx.Config.EnvoyServiceNamespace)
148-
serve.Flag("envoy-ingress-name", "Name of the Envoy ingress to inspect for Ingress status details.").PlaceHolder("<name>").StringVar(&ctx.Config.EnvoyIngressName)
149-
serve.Flag("envoy-ingress-namespace", "Envoy Ingress Namespace.").PlaceHolder("<namespace>").StringVar(&ctx.Config.EnvoyIngressNamespace)
150149

151150
serve.Flag("health-address", "Address the health HTTP endpoint will bind to.").PlaceHolder("<ipaddr>").StringVar(&ctx.healthAddr)
152151
serve.Flag("health-port", "Port the health HTTP endpoint will bind to.").PlaceHolder("<port>").IntVar(&ctx.healthPort)
@@ -169,6 +168,8 @@ func registerServe(app *kingpin.Application) (*kingpin.CmdClause, *serveContext)
169168
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)
170169
serve.Flag("leader-election-retry-period", "The interval which Contour will attempt to acquire leadership lease.").Default("2s").DurationVar(&ctx.LeaderElection.RetryPeriod)
171170

171+
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)
172+
172173
serve.Flag("root-namespaces", "Restrict contour to searching these namespaces for root ingress routes.").PlaceHolder("<ns,ns>").StringVar(&ctx.rootNamespaces)
173174

174175
serve.Flag("stats-address", "Envoy /stats interface address.").PlaceHolder("<ipaddr>").StringVar(&ctx.statsAddr)
@@ -673,87 +674,162 @@ func (s *Server) doServe() error {
673674
}
674675

675676
// Set up ingress load balancer status writer.
677+
if err := s.setupIngressLoadBalancerStatusWriter(contourConfiguration, ingressClassNames, gatewayControllerName, gatewayRef, sh.Writer()); err != nil {
678+
return err
679+
}
680+
681+
xdsServer := &xdsServer{
682+
log: s.log,
683+
registry: s.registry,
684+
config: *contourConfiguration.XDSServer,
685+
snapshotHandler: snapshotHandler,
686+
resources: resources,
687+
initialDagBuilt: contourHandler.HasBuiltInitialDag,
688+
}
689+
if err := s.mgr.Add(xdsServer); err != nil {
690+
return err
691+
}
692+
693+
notifier := &leadership.Notifier{
694+
ToNotify: append([]leadership.NeedLeaderElectionNotification{
695+
contourHandler,
696+
observer,
697+
}, needsNotification...),
698+
}
699+
if err := s.mgr.Add(notifier); err != nil {
700+
return err
701+
}
702+
703+
// GO!
704+
return s.mgr.Start(signals.SetupSignalHandler())
705+
}
706+
707+
func (s *Server) setupIngressLoadBalancerStatusWriter(
708+
contourConfiguration contour_api_v1alpha1.ContourConfigurationSpec,
709+
ingressClassNames []string,
710+
gatewayControllerName string,
711+
gatewayRef *types.NamespacedName,
712+
statusUpdater k8s.StatusUpdater) error {
676713
lbsw := &loadBalancerStatusWriter{
677714
log: s.log.WithField("context", "loadBalancerStatusWriter"),
678715
cache: s.mgr.GetCache(),
679716
lbStatus: make(chan corev1.LoadBalancerStatus, 1),
680717
ingressClassNames: ingressClassNames,
681718
gatewayControllerName: gatewayControllerName,
682719
gatewayRef: gatewayRef,
683-
statusUpdater: sh.Writer(),
720+
statusUpdater: statusUpdater,
684721
}
685722
if err := s.mgr.Add(lbsw); err != nil {
686723
return err
687724
}
688725

689-
// Register an informer to watch envoy's service if we haven't been given static details.
726+
elbs := &envoyLoadBalancerStatus{}
690727
if lbAddress := contourConfiguration.Ingress.StatusAddress; len(lbAddress) > 0 {
691-
s.log.WithField("loadbalancer-address", lbAddress).Info("Using supplied information for Ingress status")
692-
lbsw.lbStatus <- parseStatusFlag(lbAddress)
728+
elbs.Kind = "hostname"
729+
elbs.FQDNs = lbAddress
730+
} else if contourConfiguration.Envoy.LoadBalancer != "" {
731+
status, err := parseEnvoyLoadBalancerStatus(contourConfiguration.Envoy.LoadBalancer)
732+
if err != nil {
733+
return err
734+
}
735+
elbs = status
693736
} else {
737+
elbs.Kind = "service"
738+
elbs.Namespace = contourConfiguration.Envoy.Service.Namespace
739+
elbs.Name = contourConfiguration.Envoy.Service.Name
740+
}
741+
switch strings.ToLower(elbs.Kind) {
742+
case "hostname":
743+
s.log.WithField("loadbalancer-fqdns", lbAddress).Info("Using supplied hostname for Ingress status")
744+
lbsw.lbStatus <- parseStatusFlag(elbs.FQDNs)
745+
case "service":
746+
// Register an informer to watch supplied service
694747
serviceHandler := &k8s.ServiceStatusLoadBalancerWatcher{
695-
ServiceName: contourConfiguration.Envoy.Service.Name,
748+
ServiceName: elbs.Name,
696749
LBStatus: lbsw.lbStatus,
697750
Log: s.log.WithField("context", "serviceStatusLoadBalancerWatcher"),
698751
}
699752

700753
var handler cache.ResourceEventHandler = serviceHandler
701-
if contourConfiguration.Envoy.Service.Namespace != "" {
702-
handler = k8s.NewNamespaceFilter([]string{contourConfiguration.Envoy.Service.Namespace}, handler)
754+
if elbs.Namespace != "" {
755+
handler = k8s.NewNamespaceFilter([]string{elbs.Namespace}, handler)
703756
}
704757

705758
if err := s.informOnResource(&corev1.Service{}, handler); err != nil {
706-
s.log.WithError(err).WithField("resource", "services").Fatal("failed to create informer")
759+
s.log.WithError(err).WithField("resource", "services").Fatal("failed to create services informer")
707760
}
708-
761+
s.log.Infof("Watching %s for Ingress status", elbs)
762+
case "ingress":
763+
// Register an informer to watch supplied ingress
709764
ingressHandler := &k8s.IngressStatusLoadBalancerWatcher{
710-
ServiceName: contourConfiguration.Envoy.Service.Name,
765+
IngressName: elbs.Name,
711766
LBStatus: lbsw.lbStatus,
712767
Log: s.log.WithField("context", "ingressStatusLoadBalancerWatcher"),
713768
}
714769

715-
var ingressEventHandler cache.ResourceEventHandler = ingressHandler
716-
if contourConfiguration.Envoy.Ingress.Namespace != "" {
717-
handler = k8s.NewNamespaceFilter([]string{contourConfiguration.Envoy.Ingress.Namespace}, handler)
770+
var handler cache.ResourceEventHandler = ingressHandler
771+
if elbs.Namespace != "" {
772+
handler = k8s.NewNamespaceFilter([]string{elbs.Namespace}, handler)
718773
}
719774

720-
if err := informOnResource(&networking_v1.Ingress{}, ingressEventHandler, s.mgr.GetCache()); err != nil {
775+
if err := s.informOnResource(&networking_v1.Ingress{}, handler); err != nil {
721776
s.log.WithError(err).WithField("resource", "ingresses").Fatal("failed to create ingresses informer")
722777
}
778+
s.log.Infof("Watching %s for Ingress status", elbs)
779+
default:
780+
return fmt.Errorf("unsupported ingress kind: %s", elbs.Kind)
781+
}
723782

724-
s.log.WithField("envoy-service-name", contourConfiguration.Envoy.Service.Name).
725-
WithField("envoy-service-namespace", contourConfiguration.Envoy.Service.Namespace).
726-
Info("Watching Service for Ingress status")
783+
return nil
784+
}
727785

728-
s.log.WithField("envoy-ingress-name", contourConfiguration.Envoy.Ingress.Name).
729-
WithField("envoy-ingress-namespace", contourConfiguration.Envoy.Ingress.Namespace).
730-
Info("Watching Ingress for Ingress status")
731-
}
786+
type envoyLoadBalancerStatus struct {
787+
Kind string
788+
FQDNs string
789+
config.NamespacedName
790+
}
732791

733-
xdsServer := &xdsServer{
734-
log: s.log,
735-
registry: s.registry,
736-
config: *contourConfiguration.XDSServer,
737-
snapshotHandler: snapshotHandler,
738-
resources: resources,
739-
initialDagBuilt: contourHandler.HasBuiltInitialDag,
792+
func (elbs *envoyLoadBalancerStatus) String() string {
793+
if elbs.Kind == "hostname" {
794+
return fmt.Sprintf("%s:%s", elbs.Kind, elbs.FQDNs)
740795
}
741-
if err := s.mgr.Add(xdsServer); err != nil {
742-
return err
796+
return fmt.Sprintf("%s:%s/%s", elbs.Kind, elbs.Namespace, elbs.Name)
797+
}
798+
799+
func parseEnvoyLoadBalancerStatus(s string) (*envoyLoadBalancerStatus, error) {
800+
parts := strings.SplitN(s, ":", 2)
801+
if len(parts) != 2 {
802+
return nil, fmt.Errorf("invalid load-balancer-status: %s", s)
743803
}
744804

745-
notifier := &leadership.Notifier{
746-
ToNotify: append([]leadership.NeedLeaderElectionNotification{
747-
contourHandler,
748-
observer,
749-
}, needsNotification...),
805+
if parts[1] == "" {
806+
return nil, fmt.Errorf("invalid load-balancer-status: empty object reference")
750807
}
751-
if err := s.mgr.Add(notifier); err != nil {
752-
return err
808+
809+
elbs := envoyLoadBalancerStatus{}
810+
811+
elbs.Kind = strings.ToLower(parts[0])
812+
switch elbs.Kind {
813+
case "ingress", "service":
814+
parts = strings.Split(parts[1], "/")
815+
if len(parts) != 2 {
816+
return nil, fmt.Errorf("invalid load-balancer-status: %s is not in the format of <namespace>/<name>", s)
817+
}
818+
819+
if parts[0] == "" || parts[1] == "" {
820+
return nil, fmt.Errorf("invalid load-balancer-status: <namespace> or <name> is empty")
821+
}
822+
elbs.Namespace = parts[0]
823+
elbs.Name = parts[1]
824+
case "hostname":
825+
elbs.FQDNs = parts[1]
826+
case "":
827+
return nil, fmt.Errorf("invalid load-balancer-status: kind is empty")
828+
default:
829+
return nil, fmt.Errorf("invalid load-balancer-status: unsupported kind: %s", elbs.Kind)
753830
}
754831

755-
// GO!
756-
return s.mgr.Start(signals.SetupSignalHandler())
832+
return &elbs, nil
757833
}
758834

759835
func (s *Server) getExtensionSvcConfig(name string, namespace string) (xdscache_v3.ExtensionServiceConfig, error) {

cmd/contour/serve_test.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
contour_api_v1alpha1 "github.com/projectcontour/contour/apis/projectcontour/v1alpha1"
2020
"github.com/projectcontour/contour/internal/dag"
2121
"github.com/projectcontour/contour/internal/ref"
22+
"github.com/projectcontour/contour/pkg/config"
2223
"github.com/sirupsen/logrus"
2324
"github.com/stretchr/testify/assert"
2425
"github.com/stretchr/testify/require"
@@ -217,3 +218,109 @@ func mustGetIngressProcessor(t *testing.T, builder *dag.Builder) *dag.IngressPro
217218
require.FailNow(t, "IngressProcessor not found in list of DAG builder's processors")
218219
return nil
219220
}
221+
222+
func TestParseEnvoyLoadBalancerStatus(t *testing.T) {
223+
224+
tests := []struct {
225+
name string
226+
status string
227+
want envoyLoadBalancerStatus
228+
}{
229+
{
230+
name: "Service",
231+
status: "service:namespace-1/name-1",
232+
want: envoyLoadBalancerStatus{
233+
Kind: "service",
234+
NamespacedName: config.NamespacedName{
235+
Name: "name-1",
236+
Namespace: "namespace-1",
237+
},
238+
},
239+
},
240+
{
241+
name: "Ingress",
242+
status: "ingress:namespace-1/name-1",
243+
want: envoyLoadBalancerStatus{
244+
Kind: "ingress",
245+
NamespacedName: config.NamespacedName{
246+
Name: "name-1",
247+
Namespace: "namespace-1",
248+
},
249+
},
250+
},
251+
{
252+
name: "hostname",
253+
status: "hostname:example.com",
254+
want: envoyLoadBalancerStatus{
255+
Kind: "hostname",
256+
FQDNs: "example.com",
257+
},
258+
},
259+
}
260+
for _, tt := range tests {
261+
t.Run(tt.name, func(t *testing.T) {
262+
r, err := parseEnvoyLoadBalancerStatus(tt.status)
263+
assert.NoError(t, err)
264+
assert.Equal(t, tt.want, *r)
265+
})
266+
}
267+
268+
tests2 := []struct {
269+
name string
270+
status string
271+
error string
272+
}{
273+
{
274+
name: "Empty",
275+
status: "",
276+
error: "invalid",
277+
},
278+
{
279+
name: "No kind",
280+
status: ":n",
281+
error: "kind is empty",
282+
},
283+
{
284+
name: "Invalid kind",
285+
status: "test:n",
286+
error: "unsupported kind",
287+
},
288+
{
289+
name: "No reference",
290+
status: "service:",
291+
error: "empty object reference",
292+
},
293+
{
294+
name: "No colon",
295+
status: "service",
296+
error: "invalid",
297+
},
298+
{
299+
name: "No slash",
300+
status: "service:name-1",
301+
error: "not in the format",
302+
},
303+
{
304+
name: "starts with slash",
305+
status: "service:/name-1",
306+
error: "is empty",
307+
},
308+
{
309+
name: "ends with slash",
310+
status: "service:name-1/",
311+
error: "is empty",
312+
},
313+
{
314+
name: "two many slashes",
315+
status: "service:name/x/y",
316+
error: "not in the format",
317+
},
318+
}
319+
for _, tt := range tests2 {
320+
t.Run(tt.name, func(t *testing.T) {
321+
_, err := parseEnvoyLoadBalancerStatus(tt.status)
322+
assert.Error(t, err)
323+
assert.Contains(t, err.Error(), tt.error)
324+
})
325+
}
326+
}

cmd/contour/servecontext.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -544,10 +544,6 @@ func (ctx *serveContext) convertToContourConfigurationSpec() contour_api_v1alpha
544544
Name: ctx.Config.EnvoyServiceName,
545545
Namespace: ctx.Config.EnvoyServiceNamespace,
546546
},
547-
Ingress: &contour_api_v1alpha1.NamespacedName{
548-
Name: ctx.Config.EnvoyIngressName,
549-
Namespace: ctx.Config.EnvoyIngressNamespace,
550-
},
551547
HTTPListener: &contour_api_v1alpha1.EnvoyListener{
552548
Address: ctx.httpAddr,
553549
Port: ctx.httpPort,
@@ -581,6 +577,7 @@ func (ctx *serveContext) convertToContourConfigurationSpec() contour_api_v1alpha
581577
XffNumTrustedHops: &ctx.Config.Network.XffNumTrustedHops,
582578
EnvoyAdminPort: &ctx.Config.Network.EnvoyAdminPort,
583579
},
580+
LoadBalancer: ctx.Config.LoadBalancerStatus,
584581
},
585582
Gateway: gatewayConfig,
586583
HTTPProxy: &contour_api_v1alpha1.HTTPProxyConfig{

0 commit comments

Comments
 (0)