Skip to content
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
22 changes: 22 additions & 0 deletions apis/projectcontour/v1alpha1/contourdeployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,28 @@ type NetworkPublishing struct {
//
// +optional
ServiceAnnotations map[string]string `json:"serviceAnnotations,omitempty"`

// LoadBalancerSourceRanges restricts traffic through the cloud-provider
// load balancer to the specified client IPs. This field will be ignored if
// the cloud-provider does not support this feature.
//
// See: https://kubernetes.io/docs/tasks/access-application-cluster/configure-cloud-provider-firewall/
//
// +optional
LoadBalancerSourceRanges []string `json:"loadBalancerSourceRanges,omitempty"`

// LoadBalancerClass is the class of the load balancer implementation this
// Service belongs to. This field can only be set when the type is
// LoadBalancerService. Setting this field on a Service of a different type
// has no effect. This field can only be set when creating or updating a
// Service to the LoadBalancerService type. Once set, it cannot be changed.
// The value of this field corresponds to the value of the
// spec.loadBalancerClass field on the Kubernetes Service.
//
// See: https://kubernetes.io/docs/concepts/services-networking/service/#load-balancer-class
//
// +optional
LoadBalancerClass *string `json:"loadBalancerClass,omitempty"`
}

// NetworkPublishingType is a way to publish network endpoints.
Expand Down
10 changes: 10 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.

20 changes: 20 additions & 0 deletions examples/contour/01-crds.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3792,6 +3792,26 @@ spec:
a single IP family on single-stack clusters), or "RequireDualStack"
(two IP families on dual-stack configured clusters, otherwise fail).
type: string
loadBalancerClass:
description: |-
LoadBalancerClass is the class of the load balancer implementation this
Service belongs to. This field can only be set when the type is
LoadBalancerService. Setting this field on a Service of a different type
has no effect. This field can only be set when creating or updating a
Service to the LoadBalancerService type. Once set, it cannot be changed.
The value of this field corresponds to the value of the
spec.loadBalancerClass field on the Kubernetes Service.
See: https://kubernetes.io/docs/concepts/services-networking/service/#load-balancer-class
type: string
loadBalancerSourceRanges:
description: |-
LoadBalancerSourceRanges restricts traffic through the cloud-provider
load balancer to the specified client IPs. This field will be ignored if
the cloud-provider does not support this feature.
See: https://kubernetes.io/docs/tasks/access-application-cluster/configure-cloud-provider-firewall/
items:
type: string
type: array
serviceAnnotations:
additionalProperties:
type: string
Expand Down
8 changes: 8 additions & 0 deletions internal/provisioner/controller/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,14 @@ func (r *gatewayReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
}

contourModel.Spec.NetworkPublishing.Envoy.ServiceAnnotations = networkPublishing.ServiceAnnotations

if len(networkPublishing.LoadBalancerSourceRanges) > 0 {
contourModel.Spec.NetworkPublishing.Envoy.LoadBalancerSourceRanges = networkPublishing.LoadBalancerSourceRanges
}

if networkPublishing.LoadBalancerClass != nil {
contourModel.Spec.NetworkPublishing.Envoy.LoadBalancerClass = networkPublishing.LoadBalancerClass
}
}

// Node placement
Expand Down
34 changes: 34 additions & 0 deletions internal/provisioner/controller/gateway_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -962,6 +962,40 @@ func TestGatewayReconcile(t *testing.T) {
assert.Equal(t, int32(30001), svc.Spec.Ports[1].Port)
},
},
"If ContourDeployment.Spec.Envoy.NetworkPublishing sets loadBalancerSourceRanges and loadBalancerClass, the Envoy service reflects them": {
gatewayClass: reconcilableGatewayClassWithParams("gatewayclass-1", controller),
gatewayClassParams: &contour_v1alpha1.ContourDeployment{
ObjectMeta: meta_v1.ObjectMeta{
Namespace: "projectcontour",
Name: "gatewayclass-1-params",
},
Spec: contour_v1alpha1.ContourDeploymentSpec{
Envoy: &contour_v1alpha1.EnvoySettings{
NetworkPublishing: &contour_v1alpha1.NetworkPublishing{
Type: contour_v1alpha1.LoadBalancerServicePublishingType,
LoadBalancerSourceRanges: []string{"10.0.0.0/8", "192.168.0.0/16"},
LoadBalancerClass: ptr.To("example.io/my-lb"),
},
},
},
},
gateway: makeGateway(),
assertions: func(t *testing.T, r *gatewayReconciler, _ *gatewayapi_v1.Gateway, reconcileErr error) {
require.NoError(t, reconcileErr)

svc := &core_v1.Service{
ObjectMeta: meta_v1.ObjectMeta{
Namespace: "gateway-1",
Name: "envoy-gateway-1",
},
}
require.NoError(t, r.client.Get(context.Background(), keyFor(svc), svc))
assert.Equal(t, core_v1.ServiceTypeLoadBalancer, svc.Spec.Type)
assert.Equal(t, []string{"10.0.0.0/8", "192.168.0.0/16"}, svc.Spec.LoadBalancerSourceRanges)
require.NotNil(t, svc.Spec.LoadBalancerClass)
assert.Equal(t, "example.io/my-lb", *svc.Spec.LoadBalancerClass)
},
},
"If ContourDeployment.Spec.Envoy.WorkloadType is set to Deployment, an Envoy deployment is provisioned with the specified number of replicas": {
gatewayClass: reconcilableGatewayClassWithParams("gatewayclass-1", controller),
gatewayClassParams: &contour_v1alpha1.ContourDeployment{
Expand Down
10 changes: 10 additions & 0 deletions internal/provisioner/equality/equality.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,16 @@ func LoadBalancerServiceChanged(current, expected *core_v1.Service) (*core_v1.Se
changed = true
}

if !apiequality.Semantic.DeepEqual(current.Spec.LoadBalancerSourceRanges, expected.Spec.LoadBalancerSourceRanges) {
updated.Spec.LoadBalancerSourceRanges = expected.Spec.LoadBalancerSourceRanges
changed = true
}

if !apiequality.Semantic.DeepEqual(current.Spec.LoadBalancerClass, expected.Spec.LoadBalancerClass) {
updated.Spec.LoadBalancerClass = expected.Spec.LoadBalancerClass
changed = true
}

if !changed {
return nil, false
}
Expand Down
26 changes: 26 additions & 0 deletions internal/provisioner/equality/equality_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -459,13 +459,39 @@ func TestLoadBalancerServiceChanged(t *testing.T) {
},
expect: true,
},
{
description: "if load balancer source ranges changed",
mutate: func(svc *core_v1.Service) {
svc.Spec.LoadBalancerSourceRanges = []string{"10.0.0.0/8"}
},
expect: true,
},
{
description: "if load balancer source ranges unchanged",
mutate: func(_ *core_v1.Service) {
// no-op: cntr is pre-populated with source ranges in the loop below,
// so both mutated and expected have the same non-empty value.
},
expect: false,
},
{
description: "if load balancer class changed",
mutate: func(svc *core_v1.Service) {
svc.Spec.LoadBalancerClass = ptr.To("other.io/lb")
},
expect: true,
},
}

for _, tc := range testCases {

cntr.Spec.NetworkPublishing.Envoy.Type = model.LoadBalancerServicePublishingType
cntr.Spec.NetworkPublishing.Envoy.LoadBalancer.Scope = model.ExternalLoadBalancer
cntr.Spec.NetworkPublishing.Envoy.LoadBalancer.ProviderParameters.Type = model.AWSLoadBalancerProvider
cntr.Spec.NetworkPublishing.Envoy.LoadBalancerSourceRanges = nil
if tc.description == "if load balancer source ranges unchanged" {
cntr.Spec.NetworkPublishing.Envoy.LoadBalancerSourceRanges = []string{"10.0.0.0/8"}
}
if tc.description == "if load balancer IP changed" {
loadBalancerIP := "1.2.3.4"
cntr = &model.Contour{
Expand Down
8 changes: 8 additions & 0 deletions internal/provisioner/model/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,14 @@ type EnvoyNetworkPublishing struct {
//
// If unset, defaults to "Local".
ExternalTrafficPolicy core_v1.ServiceExternalTrafficPolicyType

// LoadBalancerSourceRanges restricts traffic through the cloud-provider
// load balancer to the specified client IPs.
LoadBalancerSourceRanges []string

// LoadBalancerClass is the class of the load balancer implementation
// this Service belongs to. Once set on a created Service, it cannot be changed.
LoadBalancerClass *string
}

type NetworkPublishingType = contour_v1alpha1.NetworkPublishingType
Expand Down
6 changes: 6 additions & 0 deletions internal/provisioner/objects/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,12 @@ func DesiredEnvoyService(contour *model.Contour) *core_v1.Service {
internalAnnotations := InternalLBAnnotations[provider]
maps.Copy(svc.Annotations, internalAnnotations)
}
if len(contour.Spec.NetworkPublishing.Envoy.LoadBalancerSourceRanges) > 0 {
svc.Spec.LoadBalancerSourceRanges = contour.Spec.NetworkPublishing.Envoy.LoadBalancerSourceRanges
}
if contour.Spec.NetworkPublishing.Envoy.LoadBalancerClass != nil {
svc.Spec.LoadBalancerClass = contour.Spec.NetworkPublishing.Envoy.LoadBalancerClass
}
case model.NodePortServicePublishingType:
svc.Spec.Type = core_v1.ServiceTypeNodePort

Expand Down
73 changes: 73 additions & 0 deletions internal/provisioner/objects/service/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,48 @@ func checkServiceHasLoadBalancerAddress(t *testing.T, svc *core_v1.Service, addr
}
}

func checkServiceHasLoadBalancerSourceRanges(t *testing.T, svc *core_v1.Service, ranges []string) {
t.Helper()

if len(svc.Spec.LoadBalancerSourceRanges) != len(ranges) {
t.Errorf("service has %d loadBalancerSourceRanges, want %d", len(svc.Spec.LoadBalancerSourceRanges), len(ranges))
return
}
for i, r := range ranges {
if svc.Spec.LoadBalancerSourceRanges[i] != r {
t.Errorf("service loadBalancerSourceRanges[%d] = %s, want %s", i, svc.Spec.LoadBalancerSourceRanges[i], r)
}
}
}

func checkServiceHasNoLoadBalancerSourceRanges(t *testing.T, svc *core_v1.Service) {
t.Helper()

if len(svc.Spec.LoadBalancerSourceRanges) != 0 {
t.Errorf("service has unexpected loadBalancerSourceRanges: %v", svc.Spec.LoadBalancerSourceRanges)
}
}

func checkServiceHasLoadBalancerClass(t *testing.T, svc *core_v1.Service, class string) {
t.Helper()

if svc.Spec.LoadBalancerClass == nil {
t.Errorf("service has nil loadBalancerClass, want %s", class)
return
}
if *svc.Spec.LoadBalancerClass != class {
t.Errorf("service loadBalancerClass = %s, want %s", *svc.Spec.LoadBalancerClass, class)
}
}

func checkServiceHasNoLoadBalancerClass(t *testing.T, svc *core_v1.Service) {
t.Helper()

if svc.Spec.LoadBalancerClass != nil {
t.Errorf("service has unexpected loadBalancerClass: %s", *svc.Spec.LoadBalancerClass)
}
}

func TestDesiredContourService(t *testing.T) {
name := "svc-test"
cntr := model.Default(fmt.Sprintf("%s-ns", name), name)
Expand Down Expand Up @@ -283,4 +325,35 @@ func TestDesiredEnvoyService(t *testing.T) {
svc = DesiredEnvoyService(cntr)
checkServiceHasType(t, svc, core_v1.ServiceTypeClusterIP)
checkServiceHasAnnotations(t, svc) // passing no keys means we expect no annotations

// Test LoadBalancerSourceRanges is set for LoadBalancerService type.
cntr.Spec.NetworkPublishing.Envoy.Type = model.LoadBalancerServicePublishingType
cntr.Spec.NetworkPublishing.Envoy.LoadBalancer.Scope = model.ExternalLoadBalancer
cntr.Spec.NetworkPublishing.Envoy.LoadBalancer.ProviderParameters = model.ProviderLoadBalancerParameters{Type: model.AWSLoadBalancerProvider}
cntr.Spec.NetworkPublishing.Envoy.LoadBalancerSourceRanges = []string{"10.0.0.0/8", "192.168.0.0/16"}
svc = DesiredEnvoyService(cntr)
checkServiceHasType(t, svc, core_v1.ServiceTypeLoadBalancer)
checkServiceHasLoadBalancerSourceRanges(t, svc, []string{"10.0.0.0/8", "192.168.0.0/16"})

// Test LoadBalancerSourceRanges is NOT set for NodePortService type.
cntr.Spec.NetworkPublishing.Envoy.Type = model.NodePortServicePublishingType
svc = DesiredEnvoyService(cntr)
checkServiceHasNoLoadBalancerSourceRanges(t, svc)

// Clear source ranges so they don't affect subsequent LoadBalancerClass assertions,
// since cntr is shared state across all test blocks in this function.
cntr.Spec.NetworkPublishing.Envoy.LoadBalancerSourceRanges = nil

// Test LoadBalancerClass is set for LoadBalancerService type.
lbClass := "example.io/my-lb"
cntr.Spec.NetworkPublishing.Envoy.Type = model.LoadBalancerServicePublishingType
cntr.Spec.NetworkPublishing.Envoy.LoadBalancerClass = &lbClass
svc = DesiredEnvoyService(cntr)
checkServiceHasType(t, svc, core_v1.ServiceTypeLoadBalancer)
checkServiceHasLoadBalancerClass(t, svc, lbClass)

// Test LoadBalancerClass is NOT set for NodePortService type.
cntr.Spec.NetworkPublishing.Envoy.Type = model.NodePortServicePublishingType
svc = DesiredEnvoyService(cntr)
checkServiceHasNoLoadBalancerClass(t, svc)
}
21 changes: 21 additions & 0 deletions site/content/docs/main/config/gateway-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,27 @@ All Gateways provisioned using the `contour-with-envoy-deployment` GatewayClass

See [the API documentation][6] for all `ContourDeployment` options.

For example, to restrict access to the provisioned Envoy load balancer to specific source IP ranges and to select a specific load balancer implementation class:

```yaml
kind: ContourDeployment
apiVersion: projectcontour.io/v1alpha1
metadata:
namespace: projectcontour
name: contour-with-lb-params
spec:
envoy:
networkPublishing:
type: LoadBalancerService
loadBalancerSourceRanges:
- 10.0.0.0/8
- 192.168.0.0/16
loadBalancerClass: example.io/my-lb
```

> **Note:** `loadBalancerClass` is immutable on a Kubernetes Service once set.
> To change it you must delete and re-create the Gateway.

It's important to note that, per the [GatewayClass spec][10]:

> It is recommended that [GatewayClass] be used as a template for Gateways.
Expand Down