Skip to content

Commit 9050ba2

Browse files
author
Lars Maier
authored
Merge pull request #389 from arangodb/lb-source-ranges
Added loadBalancerSourceRanges field to external-access-spec
2 parents 9e5c167 + 0f714d6 commit 9050ba2

File tree

9 files changed

+228
-7
lines changed

9 files changed

+228
-7
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ dashboard/assets.go: $(DASHBOARDSOURCES) $(DASHBOARDDIR)/Dockerfile.build
182182

183183
$(BIN): $(SOURCES) dashboard/assets.go
184184
@mkdir -p $(BINDIR)
185-
CGO_ENABLED=0 go build -installsuffix netgo -ldflags "-X main.projectVersion=$(VERSION) -X main.projectBuild=$(COMMIT)" -o $(BIN) $(REPOPATH)
185+
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -installsuffix netgo -ldflags "-X main.projectVersion=$(VERSION) -X main.projectBuild=$(COMMIT)" -o $(BIN) $(REPOPATH)
186186

187187
.PHONY: docker
188188
docker: check-vars $(BIN)

dashboard/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,5 @@
1919
npm-debug.log*
2020
yarn-debug.log*
2121
yarn-error.log*
22+
23+
assets.go

docs/Manual/Deployment/Kubernetes/DeploymentResource.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,14 @@ This setting is used when `spec.externalAccess.type` is set to `LoadBalancer` or
158158

159159
If you do not specify this setting, an IP will be chosen automatically by the load-balancer provisioner.
160160

161+
### `spec.externalAccess.loadBalancerSourceRanges: []string`
162+
163+
If specified and supported by the platform (cloud provider), this will restrict traffic through the cloud-provider
164+
load-balancer will be restricted to the specified client IPs. This field will be ignored if the
165+
cloud-provider does not support the feature.
166+
167+
More info: https://kubernetes.io/docs/tasks/access-application-cluster/configure-cloud-provider-firewall/
168+
161169
### `spec.externalAccess.nodePort: int`
162170

163171
This setting specifies the port used to expose the ArangoDB deployment on.
@@ -254,6 +262,15 @@ This setting is used when `spec.sync.externalAccess.type` is set to `NodePort` o
254262

255263
If you do not specify this setting, a random port will be chosen automatically.
256264

265+
### `spec.sync.externalAccess.loadBalancerSourceRanges: []string`
266+
267+
If specified and supported by the platform (cloud provider), this will restrict traffic through the cloud-provider
268+
load-balancer will be restricted to the specified client IPs. This field will be ignored if the
269+
cloud-provider does not support the feature.
270+
271+
More info: https://kubernetes.io/docs/tasks/access-application-cluster/configure-cloud-provider-firewall/
272+
273+
257274
### `spec.sync.externalAccess.masterEndpoint: []string`
258275

259276
This setting specifies the master endpoint(s) advertised by the ArangoSync SyncMasters.

pkg/apis/deployment/v1alpha/external_access_spec.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ package v1alpha
2424

2525
import (
2626
"fmt"
27+
"net"
2728
"net/url"
2829

2930
"github.com/arangodb/kube-arangodb/pkg/util"
@@ -37,6 +38,11 @@ type ExternalAccessSpec struct {
3738
NodePort *int `json:"nodePort,omitempty"`
3839
// Optional IP used to configure a load-balancer on, in case of Auto or LoadBalancer type.
3940
LoadBalancerIP *string `json:"loadBalancerIP,omitempty"`
41+
// If specified and supported by the platform, this will restrict traffic through the cloud-provider
42+
// load-balancer will be restricted to the specified client IPs. This field will be ignored if the
43+
// cloud-provider does not support the feature.
44+
// More info: https://kubernetes.io/docs/tasks/access-application-cluster/configure-cloud-provider-firewall/
45+
LoadBalancerSourceRanges []string `json:"loadBalancerSourceRanges,omitempty"`
4046
// Advertised Endpoint is passed to the coordinators/single servers for advertising a specific endpoint
4147
AdvertisedEndpoint *string `json:"advertisedEndpoint,omitempty"`
4248
}
@@ -77,6 +83,11 @@ func (s ExternalAccessSpec) Validate() error {
7783
return maskAny(fmt.Errorf("Failed to parse advertised endpoint '%s': %s", ep, err))
7884
}
7985
}
86+
for _, x := range s.LoadBalancerSourceRanges {
87+
if _, _, err := net.ParseCIDR(x); err != nil {
88+
return maskAny(fmt.Errorf("Failed to parse loadbalancer source range '%s': %s", x, err))
89+
}
90+
}
8091
return nil
8192
}
8293

@@ -95,6 +106,9 @@ func (s *ExternalAccessSpec) SetDefaultsFrom(source ExternalAccessSpec) {
95106
if s.LoadBalancerIP == nil {
96107
s.LoadBalancerIP = util.NewStringOrNil(source.LoadBalancerIP)
97108
}
109+
if s.LoadBalancerSourceRanges == nil && len(source.LoadBalancerSourceRanges) > 0 {
110+
s.LoadBalancerSourceRanges = append([]string{}, source.LoadBalancerSourceRanges...)
111+
}
98112
if s.AdvertisedEndpoint == nil {
99113
s.AdvertisedEndpoint = source.AdvertisedEndpoint
100114
}

pkg/apis/deployment/v1alpha/zz_generated.deepcopy.go

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/deployment/resources/services.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
package resources
2424

2525
import (
26+
"strings"
2627
"time"
2728

2829
v1 "k8s.io/api/core/v1"
@@ -143,7 +144,9 @@ func (r *Resources) ensureExternalAccessServices(svcs k8sutil.ServiceInterface,
143144
eaServiceType := spec.GetType().AsServiceType() // Note: Type auto defaults to ServiceTypeLoadBalancer
144145
if existing, err := svcs.Get(eaServiceName, metav1.GetOptions{}); err == nil {
145146
// External access service exists
147+
updateExternalAccessService := false
146148
loadBalancerIP := spec.GetLoadBalancerIP()
149+
loadBalancerSourceRanges := spec.LoadBalancerSourceRanges
147150
nodePort := spec.GetNodePort()
148151
if spec.GetType().IsNone() {
149152
if noneIsClusterIP {
@@ -179,12 +182,22 @@ func (r *Resources) ensureExternalAccessServices(svcs k8sutil.ServiceInterface,
179182
deleteExternalAccessService = true // Remove the current and replace with proper one
180183
createExternalAccessService = true
181184
}
185+
if strings.Join(existing.Spec.LoadBalancerSourceRanges, ",") != strings.Join(loadBalancerSourceRanges, ",") {
186+
updateExternalAccessService = true
187+
existing.Spec.LoadBalancerSourceRanges = loadBalancerSourceRanges
188+
}
182189
} else if spec.GetType().IsNodePort() {
183190
if existing.Spec.Type != v1.ServiceTypeNodePort || len(existing.Spec.Ports) != 1 || (nodePort != 0 && existing.Spec.Ports[0].NodePort != int32(nodePort)) {
184191
deleteExternalAccessService = true // Remove the current and replace with proper one
185192
createExternalAccessService = true
186193
}
187194
}
195+
if updateExternalAccessService && !createExternalAccessService && !deleteExternalAccessService {
196+
if _, err := svcs.Update(existing); err != nil {
197+
log.Debug().Err(err).Msgf("Failed to update %s external access service", title)
198+
return maskAny(err)
199+
}
200+
}
188201
} else if k8sutil.IsNotFound(err) {
189202
// External access service does not exist
190203
if !spec.GetType().IsNone() || noneIsClusterIP {
@@ -202,7 +215,8 @@ func (r *Resources) ensureExternalAccessServices(svcs k8sutil.ServiceInterface,
202215
// Let's create or update the database external access service
203216
nodePort := spec.GetNodePort()
204217
loadBalancerIP := spec.GetLoadBalancerIP()
205-
_, newlyCreated, err := k8sutil.CreateExternalAccessService(svcs, eaServiceName, svcRole, apiObject, eaServiceType, port, nodePort, loadBalancerIP, apiObject.AsOwner())
218+
loadBalancerSourceRanges := spec.LoadBalancerSourceRanges
219+
_, newlyCreated, err := k8sutil.CreateExternalAccessService(svcs, eaServiceName, svcRole, apiObject, eaServiceType, port, nodePort, loadBalancerIP, loadBalancerSourceRanges, apiObject.AsOwner())
206220
if err != nil {
207221
log.Debug().Err(err).Msgf("Failed to create %s external access service", title)
208222
return maskAny(err)

pkg/util/k8sutil/services.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import (
3636
// ServiceInterface has methods to work with Service resources.
3737
type ServiceInterface interface {
3838
Create(*v1.Service) (*v1.Service, error)
39+
Update(*v1.Service) (*v1.Service, error)
3940
Delete(name string, options *metav1.DeleteOptions) error
4041
Get(name string, options metav1.GetOptions) (*v1.Service, error)
4142
}
@@ -120,7 +121,7 @@ func CreateHeadlessService(svcs ServiceInterface, deployment metav1.Object, owne
120121
}
121122
publishNotReadyAddresses := true
122123
serviceType := v1.ServiceTypeClusterIP
123-
newlyCreated, err := createService(svcs, svcName, deploymentName, deployment.GetNamespace(), ClusterIPNone, "", serviceType, ports, "", publishNotReadyAddresses, owner)
124+
newlyCreated, err := createService(svcs, svcName, deploymentName, deployment.GetNamespace(), ClusterIPNone, "", serviceType, ports, "", nil, publishNotReadyAddresses, owner)
124125
if err != nil {
125126
return "", false, maskAny(err)
126127
}
@@ -149,7 +150,7 @@ func CreateDatabaseClientService(svcs ServiceInterface, deployment metav1.Object
149150
}
150151
serviceType := v1.ServiceTypeClusterIP
151152
publishNotReadyAddresses := false
152-
newlyCreated, err := createService(svcs, svcName, deploymentName, deployment.GetNamespace(), "", role, serviceType, ports, "", publishNotReadyAddresses, owner)
153+
newlyCreated, err := createService(svcs, svcName, deploymentName, deployment.GetNamespace(), "", role, serviceType, ports, "", nil, publishNotReadyAddresses, owner)
153154
if err != nil {
154155
return "", false, maskAny(err)
155156
}
@@ -160,7 +161,7 @@ func CreateDatabaseClientService(svcs ServiceInterface, deployment metav1.Object
160161
// If the service already exists, nil is returned.
161162
// If another error occurs, that error is returned.
162163
// The returned bool is true if the service is created, or false when the service already existed.
163-
func CreateExternalAccessService(svcs ServiceInterface, svcName, role string, deployment metav1.Object, serviceType v1.ServiceType, port, nodePort int, loadBalancerIP string, owner metav1.OwnerReference) (string, bool, error) {
164+
func CreateExternalAccessService(svcs ServiceInterface, svcName, role string, deployment metav1.Object, serviceType v1.ServiceType, port, nodePort int, loadBalancerIP string, loadBalancerSourceRanges []string, owner metav1.OwnerReference) (string, bool, error) {
164165
deploymentName := deployment.GetName()
165166
ports := []v1.ServicePort{
166167
v1.ServicePort{
@@ -171,7 +172,7 @@ func CreateExternalAccessService(svcs ServiceInterface, svcName, role string, de
171172
},
172173
}
173174
publishNotReadyAddresses := false
174-
newlyCreated, err := createService(svcs, svcName, deploymentName, deployment.GetNamespace(), "", role, serviceType, ports, loadBalancerIP, publishNotReadyAddresses, owner)
175+
newlyCreated, err := createService(svcs, svcName, deploymentName, deployment.GetNamespace(), "", role, serviceType, ports, loadBalancerIP, loadBalancerSourceRanges, publishNotReadyAddresses, owner)
175176
if err != nil {
176177
return "", false, maskAny(err)
177178
}
@@ -183,7 +184,7 @@ func CreateExternalAccessService(svcs ServiceInterface, svcName, role string, de
183184
// If another error occurs, that error is returned.
184185
// The returned bool is true if the service is created, or false when the service already existed.
185186
func createService(svcs ServiceInterface, svcName, deploymentName, ns, clusterIP, role string, serviceType v1.ServiceType,
186-
ports []v1.ServicePort, loadBalancerIP string, publishNotReadyAddresses bool, owner metav1.OwnerReference) (bool, error) {
187+
ports []v1.ServicePort, loadBalancerIP string, loadBalancerSourceRanges []string, publishNotReadyAddresses bool, owner metav1.OwnerReference) (bool, error) {
187188
labels := LabelsForDeployment(deploymentName, role)
188189
svc := &v1.Service{
189190
ObjectMeta: metav1.ObjectMeta{
@@ -203,6 +204,7 @@ func createService(svcs ServiceInterface, svcName, deploymentName, ns, clusterIP
203204
ClusterIP: clusterIP,
204205
PublishNotReadyAddresses: publishNotReadyAddresses,
205206
LoadBalancerIP: loadBalancerIP,
207+
LoadBalancerSourceRanges: loadBalancerSourceRanges,
206208
},
207209
}
208210
addOwnerRefToObject(svc.GetObjectMeta(), &owner)

pkg/util/k8sutil/services_cache.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,15 @@ func (sc *servicesCache) Create(s *v1.Service) (*v1.Service, error) {
5858
return result, nil
5959
}
6060

61+
func (sc *servicesCache) Update(s *v1.Service) (*v1.Service, error) {
62+
sc.cache = nil
63+
result, err := sc.cli.Update(s)
64+
if err != nil {
65+
return nil, maskAny(err)
66+
}
67+
return result, nil
68+
}
69+
6170
func (sc *servicesCache) Delete(name string, options *metav1.DeleteOptions) error {
6271
sc.cache = nil
6372
if err := sc.cli.Delete(name, options); err != nil {
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
//
2+
// DISCLAIMER
3+
//
4+
// Copyright 2019 ArangoDB GmbH, Cologne, Germany
5+
//
6+
// Licensed under the Apache License, Version 2.0 (the "License");
7+
// you may not use this file except in compliance with the License.
8+
// You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing, software
13+
// distributed under the License is distributed on an "AS IS" BASIS,
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
// See the License for the specific language governing permissions and
16+
// limitations under the License.
17+
//
18+
// Copyright holder is ArangoDB GmbH, Cologne, Germany
19+
//
20+
// Author Ewout Prangsma
21+
// Author Max Neunhoeffer
22+
//
23+
24+
package tests
25+
26+
import (
27+
"context"
28+
"testing"
29+
"time"
30+
31+
"github.com/dchest/uniuri"
32+
33+
api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1alpha"
34+
"github.com/arangodb/kube-arangodb/pkg/client"
35+
"github.com/arangodb/kube-arangodb/pkg/util"
36+
"github.com/arangodb/kube-arangodb/pkg/util/k8sutil"
37+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
38+
)
39+
40+
// tests cursor forwarding with load-balanced conn., specify a source range
41+
func TestLoadBalancingSourceRanges(t *testing.T) {
42+
longOrSkip(t)
43+
44+
c := client.MustNewInCluster()
45+
kubecli := mustNewKubeClient(t)
46+
ns := getNamespace(t)
47+
48+
// Prepare deployment config
49+
namePrefix := "test-lb-src-ranges-"
50+
depl := newDeployment(namePrefix + uniuri.NewLen(4))
51+
depl.Spec.Mode = api.NewMode(api.DeploymentModeCluster)
52+
depl.Spec.Image = util.NewString("arangodb/arangodb:latest")
53+
depl.Spec.ExternalAccess.Type = api.NewExternalAccessType(api.ExternalAccessTypeLoadBalancer)
54+
depl.Spec.ExternalAccess.LoadBalancerSourceRanges = append(depl.Spec.ExternalAccess.LoadBalancerSourceRanges, "1.2.3.0/24", "0.0.0.0/0")
55+
56+
// Create deployment
57+
_, err := c.DatabaseV1alpha().ArangoDeployments(ns).Create(depl)
58+
if err != nil {
59+
t.Fatalf("Create deployment failed: %v", err)
60+
}
61+
// Prepare cleanup
62+
defer removeDeployment(c, depl.GetName(), ns)
63+
64+
// Wait for deployment to be ready
65+
apiObject, err := waitUntilDeployment(c, depl.GetName(), ns, deploymentIsReady())
66+
if err != nil {
67+
t.Fatalf("Deployment not running in time: %v", err)
68+
}
69+
70+
// Create a database client
71+
ctx := context.Background()
72+
clOpts := &DatabaseClientOptions{
73+
UseVST: false,
74+
ShortTimeout: true,
75+
}
76+
client := mustNewArangodDatabaseClient(ctx, kubecli, apiObject, t, clOpts)
77+
78+
// Wait for cluster to be available
79+
if err := waitUntilVersionUp(client, nil); err != nil {
80+
t.Fatalf("Cluster not running returning version in time: %v", err)
81+
}
82+
83+
// Now let's use the k8s api to check if the source ranges are present in
84+
// the external service spec:
85+
svcs := kubecli.CoreV1().Services(ns)
86+
eaServiceName := k8sutil.CreateDatabaseExternalAccessServiceName(depl.GetName())
87+
// Just in case, give the service some time to appear, it should usually
88+
// be there already, when the deployment is ready, however, we have had
89+
// unstable tests in the past
90+
counter := 0
91+
var foundExternalIP string
92+
for {
93+
if svc, err := svcs.Get(eaServiceName, metav1.GetOptions{}); err == nil {
94+
spec := svc.Spec
95+
ranges := spec.LoadBalancerSourceRanges
96+
if len(ranges) != 2 {
97+
t.Errorf("LoadBalancerSourceRanges does not have length 2: %v", ranges)
98+
} else {
99+
if ranges[0] != "1.2.3.0/24" {
100+
t.Errorf("Expecting first LoadBalancerSourceRange to be \"1.2.3.0/24\", but ranges are: %v", ranges)
101+
}
102+
if ranges[1] != "0.0.0.0/0" {
103+
t.Errorf("Expecting second LoadBalancerSourceRange to be \"0.0.0.0/0\", but ranges are: %v", ranges)
104+
}
105+
}
106+
foundExternalIP = spec.LoadBalancerIP
107+
break
108+
}
109+
t.Logf("Service %s cannot be found, waiting for some time...", eaServiceName)
110+
time.Sleep(time.Second)
111+
counter += 1
112+
if counter >= 60 {
113+
t.Fatalf("Could not find service %s within 60 seconds, giving up.", eaServiceName)
114+
}
115+
}
116+
117+
// Now change the deployment spec to use different ranges:
118+
depl, err = updateDeployment(c, depl.GetName(), ns,
119+
func(spec *api.DeploymentSpec) {
120+
spec.ExternalAccess.LoadBalancerSourceRanges = []string{"4.5.0.0/16"}
121+
})
122+
if err != nil {
123+
t.Fatalf("Failed to update the deployment")
124+
}
125+
126+
// And check again:
127+
counter = 0
128+
for {
129+
time.Sleep(time.Second)
130+
if svc, err := svcs.Get(eaServiceName, metav1.GetOptions{}); err == nil {
131+
spec := svc.Spec
132+
ranges := spec.LoadBalancerSourceRanges
133+
good := true
134+
if len(ranges) != 1 {
135+
t.Logf("LoadBalancerSourceRanges does not have length 1: %v, waiting some more...", ranges)
136+
good = false
137+
} else {
138+
if ranges[0] != "4.5.0.0/16" {
139+
t.Logf("Expecting only LoadBalancerSourceRange to be \"4.5.0.0/16\", but ranges are: %v, waiting some more...", ranges)
140+
good = false
141+
} else {
142+
if spec.LoadBalancerIP != foundExternalIP {
143+
t.Errorf("Oops, the external IP of the external access service has changed: previously: %s, now: %s", foundExternalIP, spec.LoadBalancerIP)
144+
}
145+
}
146+
}
147+
if good {
148+
break
149+
}
150+
}
151+
t.Logf("Service %s cannot be found, waiting for some more time...", eaServiceName)
152+
counter += 1
153+
if counter >= 60 {
154+
t.Fatalf("Could not find changed service %s within 60 seconds, giving up.", eaServiceName)
155+
}
156+
}
157+
t.Logf("Success! Service %s was changed correctly.", eaServiceName)
158+
}

0 commit comments

Comments
 (0)