Skip to content
This repository has been archived by the owner on Jun 26, 2024. It is now read-only.

Streamline data retrieval based on annotations #407

Merged
merged 1 commit into from
May 13, 2020
Merged
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
19 changes: 10 additions & 9 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -195,17 +195,18 @@ out/test-namespace:
get-test-namespace: out/test-namespace
$(eval TEST_NAMESPACE := $(shell cat $(OUTPUT_DIR)/test-namespace))

.PHONY: deploy-e2e-crds
deploy-e2e-crds:
$(Q)kubectl --namespace $(TEST_NAMESPACE) apply -f ./test/third-party-crds/postgresql_v1alpha1_database_crd.yaml

# E2E test
.PHONY: e2e-setup
e2e-setup: e2e-cleanup
.PHONY: e2e-deploy-3rd-party-crds
e2e-deploy-3rd-party-crds: get-test-namespace
$(Q)kubectl --namespace $(TEST_NAMESPACE) apply -f ./test/third-party-crds/

.PHONY: e2e-create-namespace
e2e-create-namespace:
$(Q)kubectl create namespace $(TEST_NAMESPACE)
$(Q)kubectl --namespace $(TEST_NAMESPACE) apply -f ./test/third-party-crds/postgresql_v1alpha1_database_crd.yaml
$(Q)kubectl --namespace $(TEST_NAMESPACE) apply -f ./test/third-party-crds/etcd_v1beta2_etcdcluster_crd.yaml
$(Q)mkdir -p $(LOGS_DIR)/e2e

.PHONY: e2e-setup
e2e-setup: e2e-cleanup e2e-create-namespace e2e-deploy-3rd-party-crds
$(Q)mkdir -p ${LOGS_DIR}/e2e

.PHONY: e2e-cleanup
e2e-cleanup: get-test-namespace
Expand Down
3 changes: 1 addition & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,14 @@ require (
github.com/go-logr/logr v0.1.0
github.com/go-openapi/spec v0.19.4
github.com/google/go-containerregistry v0.0.0-20191218175032-34fb8ff33bed // indirect
github.com/imdario/mergo v0.3.8
github.com/openshift/api v3.9.1-0.20190924102528-32369d4db2ad+incompatible
github.com/openshift/custom-resource-status v0.0.0-20190822192428-e62f2f3b79f3
github.com/operator-backing-service-samples/postgresql-operator v0.0.0-20191023140509-5c3697ed3069
github.com/operator-framework/operator-lifecycle-manager v0.0.0-20191115003340-16619cd27fa5
github.com/operator-framework/operator-sdk v0.15.2
github.com/pmezard/go-difflib v1.0.0
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.4.0
gopkg.in/yaml.v2 v2.2.4
gotest.tools v2.2.0+incompatible
k8s.io/api v0.0.0
k8s.io/apiextensions-apiserver v0.0.0
Expand Down
10 changes: 0 additions & 10 deletions pkg/conditions/conditions.go

This file was deleted.

90 changes: 90 additions & 0 deletions pkg/controller/servicebindingrequest/annotations/annotations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package annotations

import (
"fmt"

"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/client-go/dynamic"
)

// bindingType encodes the medium the binding should deliver the configuration value.
type bindingType string

const (
isutton marked this conversation as resolved.
Show resolved Hide resolved
// BindingTypeVolumeMount indicates the binding should happen through a volume mount.
BindingTypeVolumeMount bindingType = "volumemount"
isutton marked this conversation as resolved.
Show resolved Hide resolved
// BindingTypeEnvVar indicates the binding should happen through environment variables.
BindingTypeEnvVar bindingType = "env"
)

// supportedBindingTypes contains all currently supported binding types.
var supportedBindingTypes = map[bindingType]bool{
BindingTypeVolumeMount: true,
BindingTypeEnvVar: true,
}

// dataPath is the path ConfigMap and Secret resources use to scope their data.
//
// note: it is currently used to provide a pointer to the "data" string, which is the location
// ConfigMap and Secret resources keep user data.
var dataPath = "data"
isutton marked this conversation as resolved.
Show resolved Hide resolved

// Result contains data that has been collected by an annotation handler.
type Result struct {
// Data contains the annotation data collected by an annotation handler inside a deep structure
// with its root being the value specified in the Path field.
Data map[string]interface{}
// Type indicates where the Object field should be injected in the application; can be either
// "env" or "volumemount".
Type bindingType
// Path is the nested location the collected data can be found in the Data field.
Path string
isutton marked this conversation as resolved.
Show resolved Hide resolved
}

// Handler should be implemented by types that want to offer a mechanism to provide binding data to
// the system.
type Handler interface {
// Handle returns binding data.
Handle() (Result, error)
}

type ErrHandlerNotFound string

func (e ErrHandlerNotFound) Error() string {
return fmt.Sprintf("could not find handler for annotation value %q", string(e))
}

func IsErrHandlerNotFound(err error) bool {
_, ok := err.(ErrHandlerNotFound)
return ok
}

// BuildHandler attempts to create an annotation handler for the given annotationKey and
// annotationValue. kubeClient is required by some annotation handlers, and an error is returned in
// the case it is required by an annotation handler but is not defined.
func BuildHandler(
isutton marked this conversation as resolved.
Show resolved Hide resolved
kubeClient dynamic.Interface,
obj *unstructured.Unstructured,
annotationKey string,
annotationValue string,
restMapper meta.RESTMapper,
) (Handler, error) {
bindingInfo, err := NewBindingInfo(annotationKey, annotationValue)
if err != nil {
return nil, err
}

val := bindingInfo.Value

switch {
case IsAttribute(val):
isutton marked this conversation as resolved.
Show resolved Hide resolved
return NewAttributeHandler(bindingInfo, *obj), nil
case IsSecret(val):
return NewSecretHandler(kubeClient, bindingInfo, *obj, restMapper)
case IsConfigMap(val):
return NewConfigMapHandler(kubeClient, bindingInfo, *obj, restMapper)
default:
return nil, ErrHandlerNotFound(val)
}
}
64 changes: 64 additions & 0 deletions pkg/controller/servicebindingrequest/annotations/attribute.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package annotations

import (
"strings"

"github.com/redhat-developer/service-binding-operator/pkg/controller/servicebindingrequest/nested"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)

const AttributeValue = "binding:env:attribute"

// AttributeHandler handles "binding:env:attribute" annotations.
type AttributeHandler struct {
isutton marked this conversation as resolved.
Show resolved Hide resolved
// inputPath is the path that should be extracted from the resource. Required.
inputPath string
// outputPath is the path the extracted data should be placed under in the
// resulting unstructured object in Handler. Required.
outputPath string
// resource is the unstructured object to extract data using inputPath. Required.
resource unstructured.Unstructured
}

// Handle returns a unstructured object according to the "binding:env:attribute"
// annotation strategy.
func (h *AttributeHandler) Handle() (Result, error) {
val, _, err := nested.GetValue(h.resource.Object, h.inputPath, h.outputPath)
isutton marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return Result{}, err
isutton marked this conversation as resolved.
Show resolved Hide resolved
}
return Result{
Data: val,
}, nil
}

// IsAttribute returns true if the annotation value should trigger the attribute
// handler.
func IsAttribute(s string) bool {
return AttributeValue == s
}

// NewAttributeHandler constructs an AttributeHandler.
func NewAttributeHandler(
bindingInfo *BindingInfo,
resource unstructured.Unstructured,
) *AttributeHandler {
outputPath := bindingInfo.SourcePath
if len(bindingInfo.ResourceReferencePath) > 0 {
outputPath = bindingInfo.ResourceReferencePath
}

// the current implementation removes "status." and "spec." from fields exported through
// annotations.
for _, prefix := range []string{"status.", "spec."} {
if strings.HasPrefix(outputPath, prefix) {
outputPath = strings.Replace(outputPath, prefix, "", 1)
}
}

return &AttributeHandler{
inputPath: bindingInfo.SourcePath,
outputPath: outputPath,
resource: resource,
}
}
132 changes: 132 additions & 0 deletions pkg/controller/servicebindingrequest/annotations/attribute_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package annotations

import (
"testing"

"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)

// TestAttributeHandler exercises the AttributeHandler's ability to extract values according to the
// given annotation name and value.
func TestAttributeHandler(t *testing.T) {
type args struct {
obj *unstructured.Unstructured
key string
value string
expected map[string]interface{}
}

assertHandler := func(args args) func(t *testing.T) {
return func(t *testing.T) {
bindingInfo, err := NewBindingInfo(args.key, args.value)
require.NoError(t, err)
require.NotNil(t, bindingInfo)
handler := NewAttributeHandler(bindingInfo, *args.obj)
got, err := handler.Handle()
require.NoError(t, err)
require.NotNil(t, got)
require.Equal(t, args.expected, got.Data)
}
}

// "scalar" tests whether a single deep scalar value can be extracted from the given object.
t.Run("should extract a single value from .status.dbConnectionIP into .dbConnectionIP",
assertHandler(args{
expected: map[string]interface{}{
"dbConnectionIP": "127.0.0.1",
},
key: "servicebindingoperator.redhat.io/status.dbConnectionIP",
value: "binding:env:attribute",
obj: &unstructured.Unstructured{
Object: map[string]interface{}{
"status": map[string]interface{}{
"dbConnectionIP": "127.0.0.1",
},
},
},
}),
)

// "scalar#alias" tests whether a single deep scalar value can be extracted from the given object
// returning a different name than the original given path.
t.Run("should extract a single value from .status.dbConnectionIP into .alias",
assertHandler(args{
expected: map[string]interface{}{
"alias": "127.0.0.1",
},
key: "servicebindingoperator.redhat.io/alias-status.dbConnectionIP",
value: "binding:env:attribute",
obj: &unstructured.Unstructured{
Object: map[string]interface{}{
"status": map[string]interface{}{
"dbConnectionIP": "127.0.0.1",
},
},
},
}),
)

// tests whether a deep slice value can be extracted from the given object.
t.Run("should extract a slice from .status.dbConnectionIPs into .dbConnectionIPs",
assertHandler(args{
expected: map[string]interface{}{
"dbConnectionIPs": []string{"127.0.0.1", "1.1.1.1"},
},
key: "servicebindingoperator.redhat.io/status.dbConnectionIPs",
value: "binding:env:attribute",
obj: &unstructured.Unstructured{
Object: map[string]interface{}{
"status": map[string]interface{}{
"dbConnectionIPs": []string{"127.0.0.1", "1.1.1.1"},
},
},
},
}),
)

// tests whether a deep map value can be extracted from the given object.
t.Run("should extract a map from .status.connection into .connection", assertHandler(args{
expected: map[string]interface{}{
"connection": map[string]interface{}{
"host": "127.0.0.1",
"port": "1234",
},
},
key: "servicebindingoperator.redhat.io/status.connection",
value: "binding:env:attribute",
obj: &unstructured.Unstructured{
Object: map[string]interface{}{
"status": map[string]interface{}{
"connection": map[string]interface{}{
"host": "127.0.0.1",
"port": "1234",
},
},
},
},
}))

// "map.key" tests whether a deep map key can be extracted from the given object.
t.Run("should extract a single map key from .status.connection into .connection",
assertHandler(args{
expected: map[string]interface{}{
"connection": map[string]interface{}{
"host": "127.0.0.1",
},
},
key: "servicebindingoperator.redhat.io/status.connection.host",
value: "binding:env:attribute",
obj: &unstructured.Unstructured{
Object: map[string]interface{}{
"status": map[string]interface{}{
"connection": map[string]interface{}{
"host": "127.0.0.1",
"port": "1234",
},
},
},
},
}),
)
}
Loading