Skip to content

Commit

Permalink
feat: add configurable serviceSelector instead of hardcoded labels (#57)
Browse files Browse the repository at this point in the history
Allow a component to configure the label selectors used to select the
services
that should be exposable. This allows Platform Routing to operator
on an unchanged target.

ServiceSelectors also allow templating of the key and value in the
configuration
to extract data from the Target CR resource.

Example:

"routing.opendatahub.io/{{.kind}}": "{{.metadata.name}}", // >
"routing.opendatahub.io/Service": "MyService"

> [!CAUTION]
> the target CR is represented as a map[string]any with lowercase
> so in contrast to the Object representation {{ .Name }} it's required
to
> write the full object path as {{ .metadata.name }}

---------

Co-authored-by: Bartosz Majsak <[email protected]>
  • Loading branch information
aslakknutsen and bartoszmajsak committed Aug 26, 2024
1 parent 53cdac1 commit 1df2c4a
Show file tree
Hide file tree
Showing 9 changed files with 145 additions and 33 deletions.
1 change: 0 additions & 1 deletion controllers/routingctrl/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,6 @@ var _ = Describe("Platform routing setup for the component", test.EnvTest(), fun
toRemove = append(toRemove, component)

// required labels for the exported service:
// routing.opendatahub.io/exported: "true"
// platform.opendatahub.io/owner-name: test-component
// platform.opendatahub.io/owner-kind: Component
addRoutingRequirementsToSvc(ctx, svc, component)
Expand Down
9 changes: 2 additions & 7 deletions controllers/routingctrl/exported_svc_locator.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,16 @@ import (
"errors"
"fmt"

"github.com/opendatahub-io/odh-platform/pkg/metadata/labels"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/client-go/util/retry"
"sigs.k8s.io/controller-runtime/pkg/client"
)

func getExportedServices(ctx context.Context, cli client.Client, target *unstructured.Unstructured) ([]corev1.Service, error) {
func getExportedServices(ctx context.Context, cli client.Client, labels map[string]string, target *unstructured.Unstructured) ([]corev1.Service, error) {
listOpts := []client.ListOption{
client.InNamespace(target.GetNamespace()),
labels.MatchingLabels(
labels.RoutingExported("true"),
labels.OwnerName(target.GetName()),
labels.OwnerKind(target.GetObjectKind().GroupVersionKind().Kind),
),
client.MatchingLabels(labels),
}

var exportedSvcList *corev1.ServiceList
Expand Down
10 changes: 4 additions & 6 deletions controllers/routingctrl/fixtures_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,18 @@ func getClusterDomain(ctx context.Context, cli client.Client) string {
return domain
}

// addRoutingRequirementsToSvc adds routing-related metadata to the Service being exported.
// It adds the "routing.opendatahub.io/exported" label to indicate that the service is exported,
// and it also sets labels for the owner component's name and kind, using
// "platform.opendatahub.io/owner-name" and "platform.opendatahub.io/owner-kind" respectively.
// addRoutingRequirementsToSvc adds routing-related metadata to the Service being exported to match the
// serviceSelector defined in the suite_test.
func addRoutingRequirementsToSvc(ctx context.Context, exportedSvc *corev1.Service, owningComponent *unstructured.Unstructured) {
exportedLabel := labels.RoutingExported("true")
ownerName := labels.OwnerName(owningComponent.GetName())
ownerKind := labels.OwnerKind(owningComponent.GetObjectKind().GroupVersionKind().Kind)

_, errExportSvc := controllerutil.CreateOrUpdate(ctx, envTest.Client, exportedSvc, func() error {
metadata.ApplyMetaOptions(exportedSvc, exportedLabel, ownerName, ownerKind)
metadata.ApplyMetaOptions(exportedSvc, ownerName, ownerKind)

return nil
})

Expect(errExportSvc).ToNot(HaveOccurred())
}

Expand Down
8 changes: 7 additions & 1 deletion controllers/routingctrl/reconcile_resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"strings"

"github.com/opendatahub-io/odh-platform/pkg/cluster"
"github.com/opendatahub-io/odh-platform/pkg/config"
"github.com/opendatahub-io/odh-platform/pkg/metadata"
"github.com/opendatahub-io/odh-platform/pkg/metadata/annotations"
"github.com/opendatahub-io/odh-platform/pkg/metadata/labels"
Expand All @@ -26,7 +27,12 @@ func (r *Controller) reconcileResources(ctx context.Context, target *unstructure

r.log.Info("Reconciling resources for target", "target", target)

exportedServices, errSvcGet := getExportedServices(ctx, r.Client, target)
renderedSelectors, errLables := config.ResolveSelectors(r.component.ServiceSelector, target)
if errLables != nil {
return fmt.Errorf("could not render labels for ServiceSelector %v. Error %w", r.component.ServiceSelector, errLables)
}

exportedServices, errSvcGet := getExportedServices(ctx, r.Client, renderedSelectors, target)
if errSvcGet != nil {
if errors.Is(errSvcGet, &ExportedServiceNotFoundError{}) {
r.log.Info("no exported services found for target", "target", target)
Expand Down
5 changes: 5 additions & 0 deletions controllers/routingctrl/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/opendatahub-io/odh-platform/controllers/routingctrl"
"github.com/opendatahub-io/odh-platform/pkg/metadata/labels"
"github.com/opendatahub-io/odh-platform/pkg/platform"
"github.com/opendatahub-io/odh-platform/pkg/spi"
"github.com/opendatahub-io/odh-platform/test"
Expand Down Expand Up @@ -37,6 +38,9 @@ var _ = SynchronizedBeforeSuite(func() {
return
}

ownerName := labels.OwnerName("{{.metadata.name}}")
ownerKind := labels.OwnerKind("{{.kind}}")

routingCtrl := routingctrl.New(
nil,
ctrl.Log.WithName("controllers").WithName("platform"),
Expand All @@ -49,6 +53,7 @@ var _ = SynchronizedBeforeSuite(func() {
Kind: "Component",
},
},
ServiceSelector: labels.MatchingLabels(ownerName, ownerKind),
},
},
routingConfiguration,
Expand Down
70 changes: 70 additions & 0 deletions pkg/config/selectors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package config

import (
"bytes"
"fmt"
"strings"
"text/template"

"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)

// ResolveSelectors uses golang template engine to resolve the expressions in the `selectorExpressions` map using
// `source` as a data input. Both the keys and values are resolved against the source data.
//
// Note: expressions are resolved against the source using lowercase keys
//
// Example source:
//
// kind: Service
// metadata:
// name: MyService
//
// Example selectorExpressions:
//
// map[string]{string} {
// "routing.opendatahub.io/{{.kind}}": "{{.metadata.name}}", // > "routing.opendatahub.io/Service": "MyService"
// }
func ResolveSelectors(selectorExpressions map[string]string, source *unstructured.Unstructured) (map[string]string, error) {
resolved := make(map[string]string, len(selectorExpressions))
mainTemplate := template.New("unused_name").Option("missingkey=error")

for key, val := range selectorExpressions {
var err error

resolvedKey := key
if strings.Contains(key, "{{") {
resolvedKey, err = resolve(mainTemplate, key, source)
if err != nil {
return nil, fmt.Errorf("could not resolve key %s: %w", key, err)
}
}

resolvedVal := val
if strings.Contains(val, "{{") {
resolvedVal, err = resolve(mainTemplate, val, source)
if err != nil {
return nil, fmt.Errorf("could not resolve value %s: %w", val, err)
}
}

resolved[resolvedKey] = resolvedVal
}

return resolved, nil
}

func resolve(templ *template.Template, textTemplate string, source *unstructured.Unstructured) (string, error) {
tmpl, err := templ.Parse(textTemplate)
if err != nil {
return "", fmt.Errorf("could not parse template: %w", err)
}

var buff bytes.Buffer

if err := tmpl.Execute(&buff, source.Object); err != nil {
return "", fmt.Errorf("could not execute template: %w", err)
}

return buff.String(), nil
}
51 changes: 51 additions & 0 deletions pkg/config/selectors_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package config_test

import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/opendatahub-io/odh-platform/pkg/config"
"github.com/opendatahub-io/odh-platform/test"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)

var _ = Describe("Templated selectors", test.Unit(), func() {

Context("simple expressions", func() {

It("should resolve simple expressions for both key and value", func() {
labels := map[string]string{
"A.{{.kind}}": "{{.metadata.name}}",
"B": "{{.kind}}",
}

target := unstructured.Unstructured{
Object: map[string]any{},
}
target.SetName("X")
target.SetKind("Y")

renderedLabels, err := config.ResolveSelectors(labels, &target)
Expect(err).ToNot(HaveOccurred())

Expect(renderedLabels["A.Y"]).To(Equal("X"))
Expect(renderedLabels["B"]).To(Equal("Y"))
})

It("should fail on missing expression", func() {
labels := map[string]string{
"A": "{{.metadata.name}}",
}

target := unstructured.Unstructured{
Object: map[string]any{},
}
target.SetKind("Y")

_, err := config.ResolveSelectors(labels, &target)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("could not execute template"))
Expect(err.Error()).To(ContainSubstring("could not resolve value"))
})
})

})
18 changes: 0 additions & 18 deletions pkg/metadata/labels/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,24 +131,6 @@ func (o OwnerUID) Value() string {
return string(o)
}

// RoutingExported is a Label to mark resources that are exported by the routing capability.
// It is intended to be set by enrolled component to mark resources that should be used to
// configure routing capability by the Platform. This can be a Kubernetes Service or Istio
// VirtualService from which settings like hosts and ports are extracted.
type RoutingExported string

func (r RoutingExported) ApplyToMeta(obj metav1.Object) {
addLabel(r, obj)
}

func (r RoutingExported) Key() string {
return "routing.opendatahub.io/exported"
}

func (r RoutingExported) Value() string {
return string(r)
}

func addLabel(label Label, obj metav1.Object) {
existingLabels := obj.GetLabels()
if existingLabels == nil {
Expand Down
6 changes: 6 additions & 0 deletions pkg/platform/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ type ObjectReference struct {
type RoutingTarget struct {
// ObjectReference provides reference details to the associated object.
ObjectReference `json:"ref,omitempty"`
// ServiceSelector is a LabelSelector definition to locate the Service(s) to expose to Routing for the given ObjectReference.
// All provided label selectors must be present on the Service to find a match.
//
// go expressions are handled in the selector key and value to set dynamic values from the current ObjectReference;
// e.g. "routing.opendatahub.io/{{.kind}}": "{{.metadata.name}}", // > "routing.opendatahub.io/Service": "MyService"
ServiceSelector map[string]string `json:"serviceSelector,omitempty"`
}

// ProtectedResource holds references and configuration details necessary for
Expand Down

0 comments on commit 1df2c4a

Please sign in to comment.