Skip to content
Closed
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
205 changes: 205 additions & 0 deletions doc/proposals/composable-integration-in sdk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
---
title: Composable integration in SDK
authors:
- "dettori@us.ibm.com"
- "luan@us.ibm.com"
- "roytman@il.ibm.com"
- "tardieu@us.ibm.com"
- "mvaziri@us.ibm.com"


reviewers:
- TBD
approvers:
- TBD
creation-date: 2019-12-16
last-updated: 2019-12-16
status: implemented
see-also:
replaces:
superseded-by:
---

# Composable integration in SDK

## Release Signoff Checklist

- \[x\] Enhancement is `implementable`
- \[x\] Design details are appropriately documented from clear requirements
- \[ \] Test plan is defined
- \[ \] Graduation criteria for dev preview, tech preview, GA
- \[ \] User-facing documentation is created in [operator-sdk/doc][operator-sdk-doc]


## Summary

Kubernetes object specifications often require constant values for their fields. When deploying an entire application
with many different resources, this limitation often results in the need for staged deployments, because some resources have to be deployed first in order to determine what data to provide for the specifications of dependent resources. This undermines the declarative nature of Kubernetes object specification and requires workflows, manual step-by-step instructions and/or brittle automated scripts for the deployment of applications as a whole.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does it mean for the referencing object and its lifecycle when the referenced object does not exist yet? Will it be created?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dmesser If the referenced object does not exist, then the referencing object's controller will keep retrying until that object is created. When that object is created, then the controller can resolve the referencing object and continue with whatever it needs to do.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting! How long would it keep trying?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That depends on the consumer of the library. In the memcached tutorial, this is done indefinitely.

We can imagine having a message in the Status of the object saying that it is waiting for some object to be created. If there is a bug in the spec of the object reference, the user could catch that and fix it. In any case, when the error is fixed or the object is created, then the controller can resolve the referencing object and move forward.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. @joelanford I think if we were to take this in, it would probably be a good default behavior to write out a status when waiting on referenced values.


The Composable SDK can be used to add cross-resource references to any existing CRD, so that values no longer
need to be hardwired. This feature allows dynamic configuration of a resource, meaning that its fields can be
resolved after it has been deployed.

See this [tutorial](https://github.com/IBM/composable/blob/master/sdk/docs/tutorial.md), in which we add cross-references to the `memcached-operator` using the Composable SDK.

The Composable SDK has been implemented and open-sourced at https://github.com/IBM/composable/tree/master/sdk.
It is also related to the Composable Operator released on operatorhub.io:
https://github.com/IBM/composable.


## Motivation

To add support for cross-resource references in CRD definitions and controllers (Composable SDK) and make this
functionality available through the Operator-SDK.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is missing: How it would be added in the SDK?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We would add the types that are here:
https://github.com/IBM/composable/blob/master/sdk/composable-types.go

so that operator developers can use the ObjectRef to specify that fields have this type.

We also add the method ResolveObject (possibly with a modified interface so that the receiver is the client instead of the first argument) as one more way in which the developer can access objects in etcd (like Get, Update etc...)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think would be nice to have your thoughts on how it should be added to the project too in the doc. Really tks for all.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok will add that.
Thanks for all the feedback!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@camilamacedo86 I changed the proposal doc according to the changes above for the interface.

## Goals

- Provide cross-resource reference types, which can be used in CRD type definitions.
- Provide resolution functions, which can be called from within a reconciler to resolve cross-resource references.


## Proposal

### User Story 1

As an operator developer, I wish not to have the value of a field be hard-wired. These are typically
values that are computed dynamically when an application is deployed. For example, the admin URL for a Kafka
deployment is known only after Kafka has been successfully deployed. If my CRD requires the URL, then
rather that requiring a string value for that field, I can define it to be a reference to another object
(perhaps the data is in the status field of the Kubernetes object managing Kafka).

### User Story 2

Often the yamls for a collection of resources require the same data in many places (e.g., a host name or port number).
Rather than hardwiring the same value everywhere, I can define cross-resource references to a unique configmap that contains
the data. This makes it easier to change the data if needed, and can allow all resources to be configured
dynamically (i.e. changes in the configmap are absorbed and resource that point to it are updated).

Copy link
Contributor

@camilamacedo86 camilamacedo86 Jan 14, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same need could be solved by just GET object in the controller and then, updating the CR resource with the values. Not necessary is required to add all values used in the CR. It can be obtained programmatically, dynamically and in realtime as well.

IHMO the user's stories needs to be improved because the idea here would not be allowed something that is not possible today. It is just to add a facility, I mean another way to get RefFrom without need to implement it.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea is that rather than hardwiring that in the controller, each CR of the CRD could have fields that point to different objects. So this gives a lot of flexibility.


### Implementation Details/Notes/Constraints (optional)

The Composable SDK offers the following types to be used in a CRD definition:

```golang
type ObjectRef struct {
GetValueFrom ComposableGetValueFrom `json:"getValueFrom"`
}

type ComposableGetValueFrom struct {
Kind string `json:"kind"`
APIVersion string `json:"apiVersion,omitempty"`
Name string `json:"name,omitempty"`
Labels []string `json:"labels,omitempty"`
Namespace string `json:"namespace,omitempty"`
Path string `json:"path"`
FormatTransformers []string `json:"format-transformers,omitempty"`
}
```

An `ObjectRef` can be used to specify the type of any field of a CRD definition, allowing the value to be determined dynamically.
For a detailed explanation of how to specify an object reference according to this schema, see [here](https://github.com/IBM/composable/blob/master/README.md#getvaluefrom-elements).

The Composable SDK offers the following types to be used as part of a Reconciler in a controller:

```golang
type ResolveObject interface {
ResolveObject(ctx context.Context, in, out interface{}) error
}

type KubernetesResourceResolver struct {
Client client.Client
ResourcesClient discovery.ServerResourcesInterface
}
```

The interface `ResolveObject` provides a function to resolve object references (see below). The struct `KubernetesResourceResolver`
implements it and can be used as part of a Reconciler struct in a CRD controller (see [tutorial](https://github.com/IBM/composable/blob/master/sdk/docs/tutorial.md)). It requires a `Client` and a
`ServerResourceInterface` used to query Kubernetes about existing resources.

A `ServerResourceInterface` can be instantiated as follows:

```golang
discovery.NewDiscoveryClientForConfigOrDie(cfg)
```

where `discovery` is the package `k8s.io/client-go/discovery`, and `cfg` is a `rest.Config`.


The Composable SDK offers the following function for resolving the value of cross-resource references.

```golang
func (k KubernetesResourceResolver) ResolveObject(ctx context.Context, object, resolved interface{}) error {
```

The function `ResolveObject` takes a context, an `object` to resolve, and a blank object
`resolved` that will contain the result of resolving cross-resource references.
It assumes that the input object has a namespace, which is then used as the default namespace when references
do not specify one. This function will cast the result to the type of the `resolved` object, provided that
appropriate data transforms have been included in the reference definitions (see [tutorial](https://github.com/IBM/composable/blob/master/sdk/docs/tutorial.md) for an example).

The `ResolveObject` function is one more way that the user can access Kubernetes objects, similar to APIs
already available in Operator-SDK, such as `r.Get(...)`, `r.Update(...)`, etc...

This function uses caching for looking up objects in order
to ensure that a consistent view of the data is obtained. If any data is not available at the time of the lookup,
it returns an error. So this function either resolves the entire object or it doesn't -- there are no partial results.
Copy link
Member

@joelanford joelanford Jan 10, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there's at least one interesting scenario that operator developer's using these object references would need to consider, and that is that the object resolves successfully and is created, and then at some point later, one of the referenced objects in a GetValueFrom field is deleted.

In that case, it looks like ResolveObject() will fail, but the caller may want to have some custom logic to handle the specific reason for the failure.

For example, if the error was a temporary connection issue, a caller would want to retry. But if the error is that a referenced object is not found, I probably still want to start retrying, but I might also want to delete the other objects that I created during a previous reconciliation that relied the resolved object that is now no longer resolving. Does that make sense?

Does the returned ComposableError.Error field have enough detail to allow this sort of decision making in the calling code?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently ComposableError does not have this level of detail.


The return value of `ResolveObject` is an `error` and the Composable SDK offers a series of functions to determine
the nature of the error. This is used to decide whether the error needs to be returned by the Reconcile function or not.

```golang
func IsIllFormedRef(err error) bool

func IsKindNotFound(err error) bool

func IsObjectNotFound(err error) bool

func IsValueNotFound(err error) bool

func IsRefNotFound(err error) bool
```

Function `IsIllFormedRef` indicates that that a cross-resource reference is ill-formed (in which case retrying reconciliation
would probably not help). Function `IsKindNotFound` indicates that the kind of the reference does not exist.
`IsObjectNotFound` indicates that the object itself does not exist, and `IsValueNotFound` that the value within the object
does not exist. Finally, `IsRefNotFound` is true if either `IsKindNotFound`, `IsObjectNotFound`, or `IsValueNotFound` are true.


### Risks and Mitigations

Using the Composable SDK means that the CRD needs to have permission to access objects that are being referenced.
This means that the operator developer needs to consider what kinds of objects will be permitted for references and
add appropriate permissions for the RBAC rules of the CRD.

Copy link
Contributor

@camilamacedo86 camilamacedo86 Jan 8, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @vazirim,

Really thank you for share your ideas and contribute with, first at all 🥇.

I have a few questions/concerns which follow. Please let me know if I misunderstood something.

So, with this solution, in a CR of a CRD for example ( Memcached ), we can map many references for pre-exisent resources in the cluster and then, it will be used to find the data by the ResolveObject. Following code from the tutorial just to clarify my questions :-)

       // Resolve the memcached instance
	resolved := &cachev1alpha1.MemcachedResolved{}
	comperr := sdk.ResolveObject(r.client, r.config, memcached, resolved, request.Namespace)
	if comperr != nil {
		if comperr.ShouldBeReturned {
			return reconcile.Result{}, comperr.Error
		}
		reqLogger.Info("Error during resolve, but not returned", comperr.Error)
		return reconcile.Result{}, nil
	}
  • How it will deal with error scenarios, for example, if 3 of 10 resources mapped for Memcached cannot be found? Will the Memcached resource be created or not? Has it a way to tell that some ref is optional?
  • If the object, for example, a ConfigMap be found but it has not the key mapped in the CR how will it work?
  • How would be the performance of it if the user adds too many resources refs to be "resolved"?
  • How to validate the data in the CR? Has it already something implemented for that?
  • Also, I think that would not be possible to call forever the reconcile in the case of something nothing works.
  • And, in real scenarios, I am afraid that would not possible to solve it by changing the options in the manager to re-trigger the reconcile every few seconds as in the demo. It would need to have a watch/observer impl for each resource referenced.

WDYT folks @estroz @joelanford @shawn-hurley ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would add one more, which is should there be some way to specify a default if it can not be resolved?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @camilamacedo86,

Thanks for your feedback!

  • If 3 out of 10 resources cannot be found, then ResolveObject returns an error. It either resolves it all, or nothing. It also employs caching to get a consistent view. The cache is made fresh for each invocation of ResolveObject.

  • If the object is found, but the data in it doesn't exist, then ResolveObject also returns an error.

  • The performance hasn't been an issue so far because we have only seen CRs with a handful of references and no more. Resolving those references amounts to looking up in etcd. In any case, caching also alleviates performance.

  • We have a validation that happens inside the Composable operator controller (so that the underlying template is well-formed). For the Composable-SDK, since the developer is modifying the CRD definition, and the open-api spec of the CRD, we get validation through that.

  • The decision to retry is up to the consumer of the composable-SDK. But any operator will have a similar problem -- you need to decide for various error scenarios whether it makes sense to requeue another Reconcile cycle or not. So this is not specific to composable-SDK.

  • Sure, having watchers works as well. In the operators we have developed, we like to resync periodically to check for the health of what the operator is managing.

--Mandana

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shawn-hurley, yes we had thought of having defaults in the context of the Composable operator because it helped with validation (we could do a dry-run deployment). But we opted for not having defaults. The idea being that we want to have the ability of resolving data-dependencies dynamically. So if a resources has not been created yet, we retry until it's online. We have found this use case to be useful.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shawn-hurley The latest version of the Composable SDK (v.0.1.4) has support for default values.

## Design Details

### Test Plan

**Note:** *Section not required until targeted at a release.*


### Graduation Criteria

**Note:** *Section not required until targeted at a release.*


## Implementation History

Composable SDK is currently available (version sdk/v0.1.3) at: https://github.com/IBM/composable/tree/master/sdk.


## Drawbacks


## Alternatives

The operator developer can develop their own types and resolution function, but they would have to duplicate
the capability of caching and all-or-nothing approach. These functionalities are best provided as a library.

The operator developer may also use the Composable Operator to wrap an existing resource (without any changes to the CRD) and allow it to have cross-resource references (https://github.com/IBM/composable).
The Composable SDK, as opposed to the Composable Operator, allows a tighter integration
of references without the need for wrapping resources.



[operator-sdk-doc]: ../../doc