Skip to content

Commit 43ce0e7

Browse files
authored
Locate Registry with Resources (#74)
Resources rely on the registry for garbage collection, and that cannot span namespaces. This adds in support in your bindings to define where the registry should go, defaulting to the service broker namespace, but allowing a hard coded value (e.g. cluster wide singletons), or per service instance. Sadly, this does expose all the registry secrets now, but it has to happen this way unless we want to track literally everything. Fixes #73
1 parent a6fc1b1 commit 43ce0e7

File tree

10 files changed

+432
-45
lines changed

10 files changed

+432
-45
lines changed

crds/servicebroker.couchbase.com_servicebrokerconfigs.yaml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,30 @@ spec:
5959
description: Plan is the name of the service plan to bind to.
6060
minLength: 1
6161
type: string
62+
registryNamespace:
63+
description: RegistryNamespace is only relevant when used with
64+
RegistryScope in the "Explicit" mode, and specifies the exact
65+
namespace a service instance registry will be generated in.
66+
type: string
67+
registryScope:
68+
default: BrokerLocal
69+
description: RegistryScope controls where the registry for a
70+
service instance or binding is located. The service broker
71+
makes all generated resources owned by the relevant registry,
72+
so deleting a service instance means deleting the registry
73+
and letting garbage collection do the rest. What is particularly
74+
important is that resources must be located in the same namespace
75+
as their owners, or they will be garbage collected. "BrokerLocal",
76+
the default provisions service registries in the same namespace
77+
as the service broker. "Explicit" allows service registries
78+
to be hard coded to a specific namespace. "InstanceLocal"
79+
will provision service registries in the same namespace as
80+
the service instance was provisioned in.
81+
enum:
82+
- Explicit
83+
- BrokerLocal
84+
- InstanceLocal
85+
type: string
6286
service:
6387
description: Service is the name of the service offering to
6488
bind to.

documentation/modules/ROOT/pages/concepts/registry.adoc

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,31 @@ Therefore, where singleton templates have been used to generate Kubernetes resou
126126
.Registry Ownership of Resources and Resource Sharing with Singletons
127127
image::reg-gc.png[align="center"]
128128

129+
[IMPORTANT]
130+
====
131+
Kubernetes garbage collection only works when the registry and its dependent resources reside in the same namespace.
132+
Failure to observe this constraint will result in your service instances and bindings being deleted erroneously.
133+
134+
See the https://kubernetes.io/docs/concepts/workloads/controllers/garbage-collection/[offical documentation^] for additional garbage collection rules.
135+
====
136+
137+
=== Registry Scoping
138+
139+
The namespace in which a registry is generated is fully under your control, see the documentation for the configuration bindings for more details (`kubectl explain servicebrokerconfig.spec.bindings.registryScope`)
140+
The default option -- `BrokerLocal` -- maintains backward compatibility, and registries will be generated in the same namespace as the service broker.
141+
This, therefore, leads to the requirement that all service instance resources must be provisioned in that namespace.
142+
143+
Other options include an explicit namespace -- `Explicit` -- where the registry and resources are hard coded to a specific namespace, and service instance local -- `InstanceLocal` -- where the registry will be provisioned in the same namespace as the service instance, as dictated by the namespace provided in the service instance creation context.
144+
145+
== Registry Directory
146+
147+
Garbage collection only works when the registry is located in the same namespace as the resources that it is associated with.
148+
The Service Broker only knows the exact namespace a service will be provisioned in on service instance creation, as provided by the request context.
149+
In order to keep track of what namespace contains the service instance and binding registries, the Service Broker maintains a directory.
150+
151+
The directory is a simple map from service instance ID to a JSON document that records the service instance namespace.
152+
Thus, when a request to get or poll a service instance is made, where the namespace is unknown, the Service Broker can interrogate its directory and determine the correct namespace to use to look for the relevant registries.
153+
129154
== Next Steps
130155

131156
We have now covered all basic configuration topics.

documentation/modules/ROOT/pages/concepts/security.adoc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,4 +128,5 @@ image::sec-static.png[align="center"]
128128

129129
== Hybrid Namespace Selection
130130

131-
Due to the flexibility of the Service Broker, you may choose to combine dynamically and statically namespaced configuration templates as you choose.
131+
Mixing the two forms of namespace selection is not supported.
132+
Read xref:concepts/registry.adoc#registry-based-garbage-collection[the registry based garbage collection] documentation for further details on why, and how to correctly configure your service instances and bindings.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright 2021 Couchbase, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package v1alpha1
16+
17+
import (
18+
"errors"
19+
)
20+
21+
// ErrResourceReferenceMissing is raised when a resource reference to another resource/attribute
22+
// is missing.
23+
var ErrResourceReferenceMissing = errors.New("resource reference missing")
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright 2021 Couchbase, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package v1alpha1
16+
17+
import (
18+
"fmt"
19+
)
20+
21+
// GetServiceAndPlanNames translates from GUIDs to human readable names used in configuration.
22+
func (config *ServiceBrokerConfig) GetServiceAndPlanNames(serviceID, planID string) (string, string, error) {
23+
for _, service := range config.Spec.Catalog.Services {
24+
if service.ID == serviceID {
25+
for _, plan := range service.Plans {
26+
if plan.ID == planID {
27+
return service.Name, plan.Name, nil
28+
}
29+
}
30+
31+
return "", "", fmt.Errorf("%w: unable to locate plan for ID %s", ErrResourceReferenceMissing, planID)
32+
}
33+
}
34+
35+
return "", "", fmt.Errorf("%w: unable to locate service for ID %s", ErrResourceReferenceMissing, serviceID)
36+
}
37+
38+
// GetTemplateBindings returns the template bindings associated with a creation request's
39+
// service and plan IDs.
40+
func (config *ServiceBrokerConfig) GetTemplateBindings(serviceID, planID string) (*ConfigurationBinding, error) {
41+
service, plan, err := config.GetServiceAndPlanNames(serviceID, planID)
42+
if err != nil {
43+
return nil, err
44+
}
45+
46+
for index, binding := range config.Spec.Bindings {
47+
if binding.Service == service && binding.Plan == plan {
48+
return &config.Spec.Bindings[index], nil
49+
}
50+
}
51+
52+
return nil, fmt.Errorf("%w: unable to locate template bindings for service plan %s/%s", ErrResourceReferenceMissing, service, plan)
53+
}

pkg/apis/servicebroker/v1alpha1/types.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,13 +266,49 @@ type RegistryValue struct {
266266
Value string `json:"value"`
267267
}
268268

269+
// RegistryScope allows the user to configure where the registry will be provisioned.
270+
// +kubebuilder:validation:Enum=Explicit;BrokerLocal;InstanceLocal
271+
type RegistryScope string
272+
273+
const (
274+
// RegistryScopeExplicit provisions the registry where you tell it to.
275+
RegistryScopeExplicit RegistryScope = "Explicit"
276+
277+
// RegistryScopeBrokerLocal provisions the registry in the same namespace
278+
// as the broker is running in.
279+
RegistryScopeBrokerLocal RegistryScope = "BrokerLocal"
280+
281+
// RegistryScopeInstanceLocal provisions the registry in the same namespace
282+
// as the service instance.
283+
RegistryScopeInstanceLocal RegistryScope = "InstanceLocal"
284+
)
285+
269286
// ConfigurationBinding binds a service plan to a set of templates
270287
// required to realize that plan.
271288
type ConfigurationBinding struct {
272289
// Name is a unique identifier for the binding.
273290
// +kubebuilder:validation:MinLength=1
274291
Name string `json:"name"`
275292

293+
// RegistryScope controls where the registry for a service instance
294+
// or binding is located. The service broker makes all generated
295+
// resources owned by the relevant registry, so deleting a service
296+
// instance means deleting the registry and letting garbage collection
297+
// do the rest. What is particularly important is that resources
298+
// must be located in the same namespace as their owners, or they will
299+
// be garbage collected. "BrokerLocal", the default provisions service
300+
// registries in the same namespace as the service broker. "Explicit"
301+
// allows service registries to be hard coded to a specific namespace.
302+
// "InstanceLocal" will provision service registries in the same
303+
// namespace as the service instance was provisioned in.
304+
// +kubebuilder:default="BrokerLocal"
305+
RegistryScope RegistryScope `json:"registryScope,omitempty"`
306+
307+
// RegistryNamespace is only relevant when used with RegistryScope in the
308+
// "Explicit" mode, and specifies the exact namespace a service instance
309+
// registry will be generated in.
310+
RegistryNamespace string `json:"registryNamespace,omitempty"`
311+
276312
// Service is the name of the service offering to bind to.
277313
// +kubebuilder:validation:MinLength=1
278314
Service string `json:"service"`

pkg/broker/handlers.go

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,14 @@ func handleCreateServiceInstance(configuration *ServerConfiguration) func(http.R
8181
return
8282
}
8383

84+
dirent, err := registerDirectoryInstance(config.Config(), request.Context, configuration.Namespace, instanceID, request.ServiceID, request.PlanID)
85+
if err != nil {
86+
jsonError(w, err)
87+
return
88+
}
89+
8490
// Check if the instance already exists.
85-
entry, err := registry.New(registry.ServiceInstance, configuration.Namespace, instanceID, false)
91+
entry, err := registry.New(registry.ServiceInstance, dirent.Namespace, instanceID, false)
8692
if err != nil {
8793
jsonError(w, err)
8894
return
@@ -333,8 +339,10 @@ func handleReadServiceInstance(configuration *ServerConfiguration) func(http.Res
333339
return
334340
}
335341

342+
dirent := getDirectoryInstance(configuration.Namespace, instanceID)
343+
336344
// Check if the instance exists.
337-
entry, err := registry.New(registry.ServiceInstance, configuration.Namespace, instanceID, true)
345+
entry, err := registry.New(registry.ServiceInstance, dirent.Namespace, instanceID, true)
338346
if err != nil {
339347
jsonError(w, err)
340348
return
@@ -449,9 +457,11 @@ func handleUpdateServiceInstance(configuration *ServerConfiguration) func(http.R
449457
return
450458
}
451459

460+
dirent := getDirectoryInstance(configuration.Namespace, instanceID)
461+
452462
// Check if the instance already exists.
453463
// Check if the instance exists.
454-
entry, err := registry.New(registry.ServiceInstance, configuration.Namespace, instanceID, false)
464+
entry, err := registry.New(registry.ServiceInstance, dirent.Namespace, instanceID, false)
455465
if err != nil {
456466
jsonError(w, err)
457467
return
@@ -562,7 +572,12 @@ func handleDeleteServiceInstance(configuration *ServerConfiguration) func(http.R
562572
return
563573
}
564574

565-
entry, err := registry.New(registry.ServiceInstance, configuration.Namespace, instanceID, false)
575+
dirent := getDirectoryInstance(configuration.Namespace, instanceID)
576+
577+
// Probably the wrong place for this...
578+
deleteDirectoryInstance(configuration.Namespace, instanceID)
579+
580+
entry, err := registry.New(registry.ServiceInstance, dirent.Namespace, instanceID, false)
566581
if err != nil {
567582
jsonError(w, err)
568583
return
@@ -653,7 +668,9 @@ func handlePollServiceInstance(configuration *ServerConfiguration) func(http.Res
653668
return
654669
}
655670

656-
entry, err := registry.New(registry.ServiceInstance, configuration.Namespace, instanceID, false)
671+
dirent := getDirectoryInstance(configuration.Namespace, instanceID)
672+
673+
entry, err := registry.New(registry.ServiceInstance, dirent.Namespace, instanceID, false)
657674
if err != nil {
658675
jsonError(w, err)
659676
return
@@ -838,7 +855,9 @@ func handleCreateServiceBinding(configuration *ServerConfiguration) func(http.Re
838855
}
839856

840857
// Check if the service instance exists.
841-
instanceEntry, err := registry.New(registry.ServiceInstance, configuration.Namespace, instanceID, true)
858+
dirent := getDirectoryInstance(configuration.Namespace, instanceID)
859+
860+
instanceEntry, err := registry.New(registry.ServiceInstance, dirent.Namespace, instanceID, true)
842861
if err != nil {
843862
jsonError(w, err)
844863
return
@@ -850,7 +869,7 @@ func handleCreateServiceBinding(configuration *ServerConfiguration) func(http.Re
850869
}
851870

852871
// Check if the binding already exists.
853-
entry, err := registry.New(registry.ServiceBinding, configuration.Namespace, bindingID, false)
872+
entry, err := registry.New(registry.ServiceBinding, dirent.Namespace, bindingID, false)
854873
if err != nil {
855874
jsonError(w, err)
856875
return
@@ -1081,7 +1100,9 @@ func handleDeleteServiceBinding(configuration *ServerConfiguration) func(http.Re
10811100
return
10821101
}
10831102

1084-
instanceEntry, err := registry.New(registry.ServiceInstance, configuration.Namespace, instanceID, true)
1103+
dirent := getDirectoryInstance(configuration.Namespace, instanceID)
1104+
1105+
instanceEntry, err := registry.New(registry.ServiceInstance, dirent.Namespace, instanceID, true)
10851106
if err != nil {
10861107
jsonError(w, err)
10871108
return
@@ -1099,7 +1120,7 @@ func handleDeleteServiceBinding(configuration *ServerConfiguration) func(http.Re
10991120
return
11001121
}
11011122

1102-
entry, err := registry.New(registry.ServiceBinding, configuration.Namespace, bindingID, false)
1123+
entry, err := registry.New(registry.ServiceBinding, dirent.Namespace, bindingID, false)
11031124
if err != nil {
11041125
jsonError(w, err)
11051126
return

pkg/broker/util.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
v1 "github.com/couchbase/service-broker/pkg/apis/servicebroker/v1alpha1"
2626
"github.com/couchbase/service-broker/pkg/errors"
2727
"github.com/couchbase/service-broker/pkg/log"
28+
"github.com/couchbase/service-broker/pkg/registry"
2829

2930
"github.com/go-openapi/jsonpointer"
3031
"github.com/go-openapi/spec"
@@ -377,3 +378,75 @@ func getNamespace(context *runtime.RawExtension, namespace string) (string, erro
377378

378379
return namespace, nil
379380
}
381+
382+
// registerDirectoryInstance allows the namespace of the registry to be chosen so garbage
383+
// collection works as intended. All service instances get a directory entry for simplicity.
384+
func registerDirectoryInstance(config *v1.ServiceBrokerConfig, context *runtime.RawExtension, namespace, instanceID, serviceID, planID string) (*registry.DirectoryEntry, error) {
385+
binding, err := config.GetTemplateBindings(serviceID, planID)
386+
if err != nil {
387+
return nil, err
388+
}
389+
390+
dirent := &registry.DirectoryEntry{}
391+
392+
switch binding.RegistryScope {
393+
case "", v1.RegistryScopeBrokerLocal:
394+
// This is the default for backwards compatibility.
395+
dirent.Namespace = namespace
396+
case v1.RegistryScopeExplicit:
397+
dirent.Namespace = binding.RegistryNamespace
398+
case v1.RegistryScopeInstanceLocal:
399+
rn, err := getNamespace(context, namespace)
400+
if err != nil {
401+
return nil, err
402+
}
403+
404+
dirent.Namespace = rn
405+
default:
406+
return nil, errors.NewConfigurationError("unable to resolve registry namespace type %s", binding.RegistryScope)
407+
}
408+
409+
directory, err := registry.NewDirectory(namespace)
410+
if err != nil {
411+
return nil, err
412+
}
413+
414+
if err := directory.Add(instanceID, dirent); err != nil {
415+
return nil, err
416+
}
417+
418+
return dirent, nil
419+
}
420+
421+
// getDirectoryInstance returns the corresponding registry namespace for a service instance.
422+
// As this is required functionality, and existing users will not have a directory, we cannot
423+
// raise any errors here, instead return the service broker namespace to maintain backward
424+
// compaibility.
425+
func getDirectoryInstance(namespace, instanceID string) *registry.DirectoryEntry {
426+
fake := &registry.DirectoryEntry{
427+
Namespace: namespace,
428+
}
429+
430+
directory, err := registry.NewDirectory(namespace)
431+
if err != nil {
432+
return fake
433+
}
434+
435+
dirent, err := directory.Lookup(instanceID)
436+
if err != nil {
437+
return fake
438+
}
439+
440+
return dirent
441+
}
442+
443+
// deleteDirectoryInstance like it's counterpart above needs to ignore errors here
444+
// as existing service instances from earlier version won't have a directory entry.
445+
func deleteDirectoryInstance(namespace, instanceID string) {
446+
directory, err := registry.NewDirectory(namespace)
447+
if err != nil {
448+
return
449+
}
450+
451+
_ = directory.Remove(instanceID)
452+
}

0 commit comments

Comments
 (0)