Note: this is a fork until we get this merged upstream or officially change maintainers. Readme will likely be outdated.
Table of Contents generated with DocToc
Note: The project uses Golang modules, in order to activate module support, please set the the GO111MODULE
environment variable to on
. How to Install and Activate Module Support
This project also contains the Composable SDK, a library that allows users to add cross-resource references to their own CRD and enable dynamic configuration. For more information and a tutorial see here.
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.
The Composable operator alleviates this problem by wrapping any resource (native Kubernetes or CRD instance) and allowing it to be specified with references to fields of other objects. These references are resolved dynamically by the Compsable controller when the data becomes available. This allows the yaml for the entire application to be deployed at once regardless of dependencies and leverages Kubernetes native mechanisms to stage the deployment of different resources.
For example, consider a Knative
KafkaSource
resource:
apiVersion: sources.eventing.knative.dev/v1alpha1
kind: KafkaSource
metadata:
name: kafka-source
spec:
consumerGroup: knative-group
bootstrapServers: my-cluster-kafka-bootstrap.kafka:9092,my-cluster0-kafka-bootstrap.kafka:9093
topics: knative-demo-topic
sink:
apiVersion: serving.knative.dev/v1
kind: Service
name: event-display
The KafkaSource
resource requires a field bootstrapServers
whose value can only be known if a Kafka service has already
been deployed successfully. So one must first deploy Kafka, obtain this data, then create the above yaml and deploy it.
With the Composable operator, this yaml can be deployed at the same time as the rest of the application, as follows:
apiVersion: ibmcloud.ibm.com/v1alpha1
kind: Composable
metadata:
name: kafka-source
spec:
template:
apiVersion: sources.eventing.knative.dev/v1alpha1
kind: KafkaSource
metadata:
name: kafka-source
spec:
consumerGroup: knative-group
bootstrapServers:
getValueFrom:
kind: Secret
name: my-kafka-binding
path: '{.data.kafka_brokers_sasl}'
format-transformers:
- "Base64ToString"
- "JsonToObject"
- "ArrayToCSString"
topics: knative-demo-topic
sink:
apiVersion: serving.knative.dev/v1
kind: Service
name: event-display
With Composable as the wrapper object, the field bootstrapServers
can be specified with a reference getValueFrom
to another object,
in this case a secret named my-kafka-binding
that contains binding information for the Kafka service (created by a different operator).
When the Composable object is deployed, the Composable controller tries to resolve this value and keeps trying if the secret has not
been created yet. Once the secret is created and the data becomes available, the Composable operator then deploys the underlying object.
Often there is data formatting mismatch between a source and a referencing field, so Composable also provides a series of handy data transformers that can be piped together in order to obtain the correct format. In this case, the base64 secret data is first decoded to obtain a Json string, which is then parsed producing an array of urls. Finally this array is transformed into a comma-separated string.
The Composable operator allows all yamls of an application to be deployed at once, in one step, by supporting cross-resource references that are resolved dynamically, and leverages native Kubernetes mechanisms to stage the deployment of a collection of resources.
To install the latest release of Composable, run the following script:
curl -sL https://raw.githubusercontent.com/IBM/composable/master/hack/install-composable.sh | bash
Composable will be installed in the composable-operator
namespace
To remove Composable, run the following script:
curl -sL https://raw.githubusercontent.com/IBM/composable/master/hack/uninstall-composable.sh | bash
Here we provide several small examples of Composable usage. See samples for more samples.
Let's assume that we have a Kubernetes Service
, which is part of another deployment, but we would like to create an automatic
binding of our deployment objects with this Service
. With help of Composable
we can automatically create a ConfigMap
with a Service
parameter(s), e.g. the port number, whose name is http
.
The Service
yaml file might looks like:
apiVersion: v1
kind: Service
metadata:
name: myservice
namespace: default
spec:
sessionAffinity: None
type: ClusterIP
selector:
app: MyApp
ports:
- name: http
protocol: TCP
port: 80
targetPort: 9376
The following file contains the Composable
definition:
apiVersion: ibmcloud.ibm.com/v1alpha1
kind: Composable
metadata:
name: to-cm
spec:
template:
apiVersion: "v1"
kind: ConfigMap
metadata:
name: myconfigmap
data:
servicePort:
getValueFrom:
kind: Service
name: myservice
namespace: default
path: '{.spec.ports[?(@.name=="http")].port}}'
format-transformers:
- ToString
You can see the detail explanation of the getValueForm
fields below, but the purpose of the object is to create a
ConfigMap
named myconfigmap
and set servicePort
to be equal to the port named http
in the Service
object named
myservice
in the default
namespace.
A Composable and a created object (myconfigmap
) will be in the same namespace, but input objects can be in any namespaces.
The Composable operator project works tightly with 2 other related projects: SolSA - Solution Service Architecture
and cloud-operators. The samples directory has 3 different
examples of creation/configuration of Service.ibmcloud.ibm.com
from the cloud-opertors
project.
Here is one of them
apiVersion: ibmcloud.ibm.com/v1alpha1
kind: Composable
metadata:
name: comp
spec:
template:
apiVersion: ibmcloud.ibm.com/v1alpha1
kind: Service
metadata:
name: mymessagehub
spec:
instancename: mymessagehub
service: Event Streams
plan:
getValueFrom:
kind: Secret
name: mysecret
path: '{.data.plan}'
format-transformers:
- "Base64ToString"
In this example, the field plan
of the Service.ibmcloud
instance is specified by referring to a secret. When the composable operator is created, its controller tries to read the secret and obtains the data needed for this field. If the secret is available, it then creates the Service.ibmcloud
resource with the proper configuration. If the secret does not exist, the Composable controller keeps re-trying until it becomes available.
Here is another example:
apiVersion: ibmcloud.ibm.com/v1alpha1
kind: Composable
metadata:
name: comp
spec:
template:
apiVersion: ibmcloud.ibm.com/v1alpha1
kind: Service
metadata:
name:
getValueFrom:
kind: ConfigMap
name: myconfigmap
namespace: default
path: {.data.name}
spec:
instancename:
getValueFrom:
kind: ConfigMap
name: myconfigmap
namespace: default
path: {.data.name}
service: Event Streams
plan:
getValueFrom:
kind: Secret
name: mysecret
namespace: default
path: {.data.planKey}
In this example, the name of the underlying Service.ibmcloud
instance is obtained from a configmap
and the same
name is used for the field instancename
. This allows flexibility in defining configurations, and promotes the reuse
of yamls by alleviating hard-wired information.
Moreover, it can be used to configure with data that is computed dynamically as a result of the deployment of some other
resource.
The getValueFrom
element can point to any K8s and its extensions object. The kind of the object is defined by the kind
element; the object name is defined by the name
elements, and finally, the path to the data is defined by the value of
the path
element, which is a string with dots as a delimiter.
The getValueFrom
element should be a single child of the parent element and can contain the following sub-fileds:
Field | Is required | Format/Type | Comments |
---|---|---|---|
kind | Yes | String | Kind of the input object |
apiVersion | No | String | Defines a K8s Api group and version of the checking object. Helps to resolve conflicts, when the same Kind defined in several API groups and there are several supported API versions |
name | Yes/No | String | Name of the input object. Either name or labels should be defined |
labels | Yes/No | [string]string | Labels of input objects. Either name or labels should be defined |
namespace | No | String | Namespace of the input object, if isn't defined, the ns of the Composable operator will be checked |
path | Yes | String | The jsonpath formatted path to the checked filed |
format-transformers | No | Array of predefined strings | Used for value type transformation, see Format transformers |
Notes:
- Ether
name
orlabels
of the input object should be defined. If neither of both the fields are defined, an error will be generated. - The labels based search should return a single input object.
- If
apiVersion
of the input object is specified, Composable controller will try to discover an input object with the given group and version.- If the given Kind is not part of the provided group, an error will be generated, despite of the Kind existence in other groups.
- If the provided version is not supported, an error will be generated, despite of other versions existence.
- Kubernetes core objects don't have group, so only version should be specified, e.g.
v1
.
- If
apiVersion
is not provided and the given Kind is part of the Kubernetes core group, the core group will be used, despite of the Kind existence in other groups. - If
apiVersion
is not provided and the given Kind exists in several groups, and doesn't exist in the Kuberntes Core group, an error will be generated.
Sometimes, types of an input value and expected output value are not compatable, in order to resolve this issue,
Composable
supports several predefined transformers. They can be defined as a string array, so output of the previous
transformer's will be input to next one.
When you define a Composable
object, it is your responsibility to put in a correct order the transformers.
Currently Composable
supports the following transformers:
Transformer | Transformation |
---|---|
ToString |
returns a native string representation of any object |
ArrayToCSString |
returns a comma-separated string from array's values |
Base64ToString |
decodes a base64 encoded string |
StringToBase64 |
encodes a string to base64 |
StringToInt |
transforms a string to an integer |
StringToFloat |
transforms a string to a float |
StringToBool |
transforms a string to boolean |
JsonToObject |
transforms a JSON string to an object |
ObjectToJson |
transforms an object to a JSON string |
The data transformation roles are:
- If there is no data transformers - original data format will be used, include complex structures such as maps or arrays.
- Transformers from the format-transformers array executed one after another according to their order. Which allows creation of data transformation pipelines. For example, the following snippet defines transformation from a base64 encoded string to a plain string and after that to integer. This transformation can be useful to retrieve data from Secrets.
format-transformers:
- Base64ToString
- StringToInt
The getValueFrom
definition includes the destination namespace
, the specified namespace is used
to look up the referenced object. Otherwise, the namespace
of the Composable
object is checked.
The template object should be created in the same namespaces
as the Composable
object. Therefore, we recommend do not
define namespace
in the template. If the namespace field is defined and its value does not equal to the Composable
object namespace, no objects will be created, and Composable
object status will contain an error.
When the Composable object is deleted, the underlying object is deleted as well. If the user deletes the underlying object manually, it is automatically recreated.
We use a jsonpath
parser from go-client
to define path to the resolving files. Here some examples:
{.data.key-name}
- returns a path to the key namedkey-name
from aConfigMap
or from aSecret
{.spec.ports[?(@.name==“http”)].port}}
- takes port value from a port namedhttp
from theports
array`
If one of the element names, e.g. a key in a ConfigMap
or Secret
, contains dots, the dots should be escaped.
For example, if we have the following Secret definition:
apiVersion: v1
data:
tls.crt: base64data
tls.key: base64data
kind: Secret
metadata:
name: data-center
namespace: default
type: Opaque
In order to access the tls.key
data, the jsonpath should be '{.data.tls\.key}'
Due to
issue #72220, jsonpath
doesn't support regular expressions
in json-path