Skip to content

Commit 07d6e01

Browse files
authored
[Feature] Add support for managed services (#1016)
1 parent fcb9b35 commit 07d6e01

File tree

8 files changed

+141
-18
lines changed

8 files changed

+141
-18
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
- (Refactor) Use cached member's clients
66
- (Feature) Move PVC resize action to high-priority plan
77
- (Feature) Remove forgotten ArangoDB jobs during restart
8+
- (Feature) Add support for managed services
89

910
## [1.2.14](https://github.com/arangodb/kube-arangodb/tree/1.2.14) (2022-07-14)
1011
- (Feature) Add ArangoSync TLS based rotation

pkg/apis/deployment/v1/external_access_spec.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,11 @@ type ExternalAccessSpec struct {
4141
// cloud-provider does not support the feature.
4242
// More info: https://kubernetes.io/docs/tasks/access-application-cluster/configure-cloud-provider-firewall/
4343
LoadBalancerSourceRanges []string `json:"loadBalancerSourceRanges,omitempty"`
44-
// Advertised Endpoint is passed to the coordinators/single servers for advertising a specific endpoint
44+
// AdvertisedEndpoint is passed to the coordinators/single servers for advertising a specific endpoint
4545
AdvertisedEndpoint *string `json:"advertisedEndpoint,omitempty"`
46+
// ManagedServiceNames keeps names of services which are not managed by KubeArangoDB.
47+
// It is only relevant when type of service is `managed`.
48+
ManagedServiceNames []string `json:"managedServiceNames,omitempty"`
4649
}
4750

4851
// GetType returns the value of type.
@@ -65,6 +68,11 @@ func (s ExternalAccessSpec) GetAdvertisedEndpoint() string {
6568
return util.StringOrDefault(s.AdvertisedEndpoint)
6669
}
6770

71+
// GetManagedServiceNames returns a list of managed service names.
72+
func (s ExternalAccessSpec) GetManagedServiceNames() []string {
73+
return s.ManagedServiceNames
74+
}
75+
6876
// HasAdvertisedEndpoint return whether an advertised endpoint was specified or not
6977
func (s ExternalAccessSpec) HasAdvertisedEndpoint() bool {
7078
return s.AdvertisedEndpoint != nil
@@ -110,6 +118,9 @@ func (s *ExternalAccessSpec) SetDefaultsFrom(source ExternalAccessSpec) {
110118
if s.AdvertisedEndpoint == nil {
111119
s.AdvertisedEndpoint = source.AdvertisedEndpoint
112120
}
121+
if s.ManagedServiceNames == nil && len(source.ManagedServiceNames) > 0 {
122+
s.ManagedServiceNames = append([]string{}, source.ManagedServiceNames...)
123+
}
113124
}
114125

115126
// ResetImmutableFields replaces all immutable fields in the given target with values from the source spec.

pkg/apis/deployment/v1/external_access_type.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,15 @@ const (
3838
ExternalAccessTypeLoadBalancer ExternalAccessType = "LoadBalancer"
3939
// ExternalAccessTypeNodePort yields a cluster with a service of type `NodePort` to provide external access
4040
ExternalAccessTypeNodePort ExternalAccessType = "NodePort"
41+
// ExternalAccessTypeManaged yields a cluster with a service which controls only selector.
42+
ExternalAccessTypeManaged ExternalAccessType = "Managed"
4143
)
4244

4345
func (t ExternalAccessType) IsNone() bool { return t == ExternalAccessTypeNone }
4446
func (t ExternalAccessType) IsAuto() bool { return t == ExternalAccessTypeAuto }
4547
func (t ExternalAccessType) IsLoadBalancer() bool { return t == ExternalAccessTypeLoadBalancer }
4648
func (t ExternalAccessType) IsNodePort() bool { return t == ExternalAccessTypeNodePort }
49+
func (t ExternalAccessType) IsManaged() bool { return t == ExternalAccessTypeManaged }
4750

4851
// AsServiceType returns the k8s ServiceType for this ExternalAccessType.
4952
// If type is "Auto", ServiceTypeLoadBalancer is returned.
@@ -62,7 +65,8 @@ func (t ExternalAccessType) AsServiceType() core.ServiceType {
6265
// Return errors when validation fails, nil on success.
6366
func (t ExternalAccessType) Validate() error {
6467
switch t {
65-
case ExternalAccessTypeNone, ExternalAccessTypeAuto, ExternalAccessTypeLoadBalancer, ExternalAccessTypeNodePort:
68+
case ExternalAccessTypeNone, ExternalAccessTypeAuto, ExternalAccessTypeLoadBalancer, ExternalAccessTypeNodePort,
69+
ExternalAccessTypeManaged:
6670
return nil
6771
default:
6872
return errors.WithStack(errors.Wrapf(ValidationError, "Unknown external access type: '%s'", string(t)))

pkg/apis/deployment/v1/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/apis/deployment/v2alpha1/external_access_spec.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,11 @@ type ExternalAccessSpec struct {
4141
// cloud-provider does not support the feature.
4242
// More info: https://kubernetes.io/docs/tasks/access-application-cluster/configure-cloud-provider-firewall/
4343
LoadBalancerSourceRanges []string `json:"loadBalancerSourceRanges,omitempty"`
44-
// Advertised Endpoint is passed to the coordinators/single servers for advertising a specific endpoint
44+
// AdvertisedEndpoint is passed to the coordinators/single servers for advertising a specific endpoint
4545
AdvertisedEndpoint *string `json:"advertisedEndpoint,omitempty"`
46+
// ManagedServiceNames keeps names of services which are not managed by KubeArangoDB.
47+
// It is only relevant when type of service is `managed`.
48+
ManagedServiceNames []string `json:"managedServiceNames,omitempty"`
4649
}
4750

4851
// GetType returns the value of type.
@@ -65,6 +68,11 @@ func (s ExternalAccessSpec) GetAdvertisedEndpoint() string {
6568
return util.StringOrDefault(s.AdvertisedEndpoint)
6669
}
6770

71+
// GetManagedServiceNames returns a list of managed service names.
72+
func (s ExternalAccessSpec) GetManagedServiceNames() []string {
73+
return s.ManagedServiceNames
74+
}
75+
6876
// HasAdvertisedEndpoint return whether an advertised endpoint was specified or not
6977
func (s ExternalAccessSpec) HasAdvertisedEndpoint() bool {
7078
return s.AdvertisedEndpoint != nil
@@ -110,6 +118,9 @@ func (s *ExternalAccessSpec) SetDefaultsFrom(source ExternalAccessSpec) {
110118
if s.AdvertisedEndpoint == nil {
111119
s.AdvertisedEndpoint = source.AdvertisedEndpoint
112120
}
121+
if s.ManagedServiceNames == nil && len(source.ManagedServiceNames) > 0 {
122+
s.ManagedServiceNames = append([]string{}, source.ManagedServiceNames...)
123+
}
113124
}
114125

115126
// ResetImmutableFields replaces all immutable fields in the given target with values from the source spec.

pkg/apis/deployment/v2alpha1/external_access_type.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,15 @@ const (
3838
ExternalAccessTypeLoadBalancer ExternalAccessType = "LoadBalancer"
3939
// ExternalAccessTypeNodePort yields a cluster with a service of type `NodePort` to provide external access
4040
ExternalAccessTypeNodePort ExternalAccessType = "NodePort"
41+
// ExternalAccessTypeManaged yields a cluster with a service which controls only selector.
42+
ExternalAccessTypeManaged ExternalAccessType = "Managed"
4143
)
4244

4345
func (t ExternalAccessType) IsNone() bool { return t == ExternalAccessTypeNone }
4446
func (t ExternalAccessType) IsAuto() bool { return t == ExternalAccessTypeAuto }
4547
func (t ExternalAccessType) IsLoadBalancer() bool { return t == ExternalAccessTypeLoadBalancer }
4648
func (t ExternalAccessType) IsNodePort() bool { return t == ExternalAccessTypeNodePort }
49+
func (t ExternalAccessType) IsManaged() bool { return t == ExternalAccessTypeManaged }
4750

4851
// AsServiceType returns the k8s ServiceType for this ExternalAccessType.
4952
// If type is "Auto", ServiceTypeLoadBalancer is returned.
@@ -62,7 +65,8 @@ func (t ExternalAccessType) AsServiceType() core.ServiceType {
6265
// Return errors when validation fails, nil on success.
6366
func (t ExternalAccessType) Validate() error {
6467
switch t {
65-
case ExternalAccessTypeNone, ExternalAccessTypeAuto, ExternalAccessTypeLoadBalancer, ExternalAccessTypeNodePort:
68+
case ExternalAccessTypeNone, ExternalAccessTypeAuto, ExternalAccessTypeLoadBalancer, ExternalAccessTypeNodePort,
69+
ExternalAccessTypeManaged:
6670
return nil
6771
default:
6872
return errors.WithStack(errors.Wrapf(ValidationError, "Unknown external access type: '%s'", string(t)))

pkg/apis/deployment/v2alpha1/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: 96 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,13 @@ import (
2828
core "k8s.io/api/core/v1"
2929
"k8s.io/apimachinery/pkg/api/equality"
3030
meta "k8s.io/apimachinery/pkg/apis/meta/v1"
31+
"k8s.io/apimachinery/pkg/types"
3132
"k8s.io/apimachinery/pkg/util/intstr"
3233

3334
api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1"
3435
"github.com/arangodb/kube-arangodb/pkg/apis/shared"
3536
"github.com/arangodb/kube-arangodb/pkg/deployment/features"
37+
"github.com/arangodb/kube-arangodb/pkg/deployment/patch"
3638
"github.com/arangodb/kube-arangodb/pkg/metrics"
3739
"github.com/arangodb/kube-arangodb/pkg/util/errors"
3840
"github.com/arangodb/kube-arangodb/pkg/util/globals"
@@ -231,17 +233,16 @@ func (r *Resources) EnsureServices(ctx context.Context, cachedStatus inspectorIn
231233
if single {
232234
role = "single"
233235
}
234-
if err := r.ensureExternalAccessServices(ctx, cachedStatus, svcs, eaServiceName, role, "database",
235-
shared.ArangoPort, false, withLeader, spec.ExternalAccess, apiObject); err != nil {
236+
if err := r.ensureExternalAccessServices(ctx, cachedStatus, svcs, eaServiceName, role, shared.ArangoPort,
237+
false, withLeader, spec.ExternalAccess, apiObject); err != nil {
236238
return errors.WithStack(err)
237239
}
238240

239241
if spec.Sync.IsEnabled() {
240242
// External (and internal) Sync master service
241243
counterMetric.Inc()
242244
eaServiceName := k8sutil.CreateSyncMasterClientServiceName(deploymentName)
243-
role := "syncmaster"
244-
if err := r.ensureExternalAccessServices(ctx, cachedStatus, svcs, eaServiceName, role, "sync",
245+
if err := r.ensureExternalAccessServices(ctx, cachedStatus, svcs, eaServiceName, api.ServerGroupSyncMastersString,
245246
shared.ArangoSyncMasterPort, true, false, spec.Sync.ExternalAccess.ExternalAccessSpec, apiObject); err != nil {
246247
return errors.WithStack(err)
247248
}
@@ -274,13 +275,18 @@ func (r *Resources) EnsureServices(ctx context.Context, cachedStatus inspectorIn
274275
return reconcileRequired.Reconcile(ctx)
275276
}
276277

277-
// EnsureServices creates all services needed to service the deployment
278+
// ensureExternalAccessServices ensures all services needed for a deployment.
278279
func (r *Resources) ensureExternalAccessServices(ctx context.Context, cachedStatus inspectorInterface.Inspector,
279-
svcs servicev1.ModInterface, eaServiceName, svcRole, title string, port int, noneIsClusterIP bool, withLeader bool,
280+
svcs servicev1.ModInterface, eaServiceName, svcRole string, port int, noneIsClusterIP bool, withLeader bool,
280281
spec api.ExternalAccessSpec, apiObject k8sutil.APIObject) error {
281-
log := r.log.Str("section", "service-ea")
282282

283-
// Database external access service
283+
if spec.GetType().IsManaged() {
284+
// Managed services should not be created or removed by the operator.
285+
return r.ensureExternalAccessManagedServices(ctx, cachedStatus, svcs, eaServiceName, svcRole, spec, apiObject,
286+
withLeader)
287+
}
288+
289+
log := r.log.Str("section", "service-ea").Str("role", svcRole).Str("service", eaServiceName)
284290
createExternalAccessService := false
285291
deleteExternalAccessService := false
286292
eaServiceType := spec.GetType().AsServiceType() // Note: Type auto defaults to ServiceTypeLoadBalancer
@@ -307,7 +313,7 @@ func (r *Resources) ensureExternalAccessServices(ctx context.Context, cachedStat
307313
// See if LoadBalancer has been configured & the service is "old enough"
308314
oldEnoughTimestamp := time.Now().Add(-1 * time.Minute) // How long does the load-balancer provisioner have to act.
309315
if len(existing.Status.LoadBalancer.Ingress) == 0 && existing.GetObjectMeta().GetCreationTimestamp().Time.Before(oldEnoughTimestamp) {
310-
log.Str("service", eaServiceName).Info("LoadBalancerIP of %s external access service is not set, switching to NodePort", title)
316+
log.Info("LoadBalancerIP of external access service is not set, switching to NodePort")
311317
createExternalAccessService = true
312318
eaServiceType = core.ServiceTypeNodePort
313319
deleteExternalAccessService = true // Remove the LoadBalancer ex service, then add the NodePort one
@@ -340,7 +346,7 @@ func (r *Resources) ensureExternalAccessServices(ctx context.Context, cachedStat
340346
return err
341347
})
342348
if err != nil {
343-
log.Err(err).Debug("Failed to update %s external access service", title)
349+
log.Err(err).Debug("Failed to update external access service")
344350
return errors.WithStack(err)
345351
}
346352
}
@@ -352,12 +358,12 @@ func (r *Resources) ensureExternalAccessServices(ctx context.Context, cachedStat
352358
}
353359

354360
if deleteExternalAccessService {
355-
log.Str("service", eaServiceName).Info("Removing obsolete %s external access service", title)
361+
log.Info("Removing obsolete external access service")
356362
err := globals.GetGlobalTimeouts().Kubernetes().RunWithTimeout(ctx, func(ctxChild context.Context) error {
357363
return svcs.Delete(ctxChild, eaServiceName, meta.DeleteOptions{})
358364
})
359365
if err != nil {
360-
log.Err(err).Debug("Failed to remove %s external access service", title)
366+
log.Err(err).Debug("Failed to remove external access service")
361367
return errors.WithStack(err)
362368
}
363369
}
@@ -371,12 +377,88 @@ func (r *Resources) ensureExternalAccessServices(ctx context.Context, cachedStat
371377
_, newlyCreated, err := k8sutil.CreateExternalAccessService(ctxChild, svcs, eaServiceName, svcRole, apiObject,
372378
eaServiceType, port, nodePort, loadBalancerIP, loadBalancerSourceRanges, apiObject.AsOwner(), withLeader)
373379
if err != nil {
374-
log.Err(err).Debug("Failed to create %s external access service", title)
380+
log.Err(err).Debug("Failed to create external access service")
375381
return errors.WithStack(err)
376382
}
377383
if newlyCreated {
378-
log.Str("service", eaServiceName).Debug("Created %s external access service", title)
384+
log.Debug("Created %s external access service")
379385
}
380386
}
381387
return nil
382388
}
389+
390+
// ensureExternalAccessServices ensures if there are correct selectors on a managed services.
391+
// If hardcoded external service names are not on the list of managed services then it will be checked additionally.
392+
func (r *Resources) ensureExternalAccessManagedServices(ctx context.Context, cachedStatus inspectorInterface.Inspector,
393+
services servicev1.ModInterface, eaServiceName, svcRole string, spec api.ExternalAccessSpec,
394+
apiObject k8sutil.APIObject, withLeader bool) error {
395+
396+
log := r.log.Str("section", "service-ea").Str("role", svcRole).Str("service", eaServiceName)
397+
managedServiceNames := spec.GetManagedServiceNames()
398+
deploymentName := apiObject.GetName()
399+
var selector map[string]string
400+
if withLeader {
401+
selector = k8sutil.LabelsForLeaderMember(deploymentName, svcRole, "")
402+
} else {
403+
selector = k8sutil.LabelsForDeployment(deploymentName, svcRole)
404+
}
405+
406+
// Check if hardcoded service has correct selector.
407+
if svc, ok := cachedStatus.Service().V1().GetSimple(eaServiceName); !ok {
408+
// Hardcoded service (e.g. <deplname>-ea or <deplname>-sync) is not mandatory in `managed` type.
409+
if len(managedServiceNames) == 0 {
410+
log.Warn("the field \"spec.externalAccess.managedServiceNames\" should be provided for \"managed\" service type")
411+
return nil
412+
}
413+
} else if changed, err := ensureManagedServiceSelector(ctx, selector, svc, services); err != nil {
414+
return errors.WithMessage(err, "failed to ensure service selector")
415+
} else if changed {
416+
log.Info("selector applied to the managed service \"%s\"", svc.GetName())
417+
}
418+
419+
for _, svcName := range managedServiceNames {
420+
if svcName == eaServiceName {
421+
// Hardcoded service has been applied before this loop.
422+
continue
423+
}
424+
425+
svc, ok := cachedStatus.Service().V1().GetSimple(svcName)
426+
if !ok {
427+
log.Warn("managed service \"%s\" should have existed", svcName)
428+
continue
429+
}
430+
431+
if changed, err := ensureManagedServiceSelector(ctx, selector, svc, services); err != nil {
432+
return errors.WithMessage(err, "failed to ensure service selector")
433+
} else if changed {
434+
log.Info("selector applied to the managed service \"%s\"", svcName)
435+
}
436+
}
437+
438+
return nil
439+
}
440+
441+
// ensureManagedServiceSelector ensures if there is correct selector on a service.
442+
func ensureManagedServiceSelector(ctx context.Context, selector map[string]string, svc *core.Service,
443+
services servicev1.ModInterface) (bool, error) {
444+
for key, value := range selector {
445+
if currentValue, ok := svc.Spec.Selector[key]; ok && value == currentValue {
446+
continue
447+
}
448+
449+
p := patch.NewPatch()
450+
p.ItemReplace(patch.NewPath("spec", "selector"), selector)
451+
data, err := p.Marshal()
452+
if err != nil {
453+
return false, errors.WithMessage(err, "failed to marshal service selector")
454+
}
455+
456+
if _, err = services.Patch(ctx, svc.GetName(), types.JSONPatchType, data, meta.PatchOptions{}); err != nil {
457+
return false, errors.WithMessage(err, "failed to patch service selector")
458+
}
459+
460+
return true, nil
461+
}
462+
463+
return false, nil
464+
}

0 commit comments

Comments
 (0)