Working with Crossplane Functions
Crossplane introduced Functions
in version v1.11 as an Alpha feature: docs.crossplane.io/knowledge-base/guides/composition-functions.
Function is a container to which is passed FunctionIO yaml object as stdin. FunctionIO object is created and managed by Crossplane to keep state of our resources. It has 3 main entries:
-
observed: this one describes what is current state of our resources, it’s read-only, all changes will be discarded
-
desired: this one is state we want to have and it’s place where we can add aditional resources or manage already existing ones
-
results: this one is a logger, we can create log entries with 3 levels of severity: Normal, Warning and Fatal - Fatal breaks function’s code execution
What can we achieve thank to Crossplane functions:
-
create resource (Kubernetes compliant)
-
read resource (Kubernetes compliant)
-
update resource (Kubernetes compliant)
-
delete resource (Kubernetes compliant)
-
introduce variatic behavior to our compositions (if/else equivalents)
-
connect to external APIs to get/push some infromations
Preparation step 1
To start working with Functions we must enable alfa features in Crossplane (at least in version v1.11)
helm install crossplane --namespace crossplane-system crossplane-stable/crossplane \
--create-namespace \
--set "args={--debug,--enable-composition-functions}" \
--set "xfn.enabled=true" \
--set "xfn.args={--debug}"
Preparation step 2
Next step is to declare our Function in our managed resource (Composition). It’s highly recommended to use other container registry than hub.docker.io especially for development because their pull limit can block Your dev environment
spec:
compositeTypeRef:
apiVersion: vshn.appcat.vshn.io/v1
kind: XVSHNPostgreSQL
functions:
- name: fnio
type: Container
container:
image: ghcr.io/wejdross/fnio
imagePullPolicy: Always
Example function 1 - Golang
As a result of code below there will be ConfigMap created even though it wasn’t declared in original composition. This code simply takes input from stdin and unmarshal it into xfnv1alpha1.FunctionIO{}, further we append current list of desired resources by our new resource and print it to stdout. You can see that we also create log entry which will print whole aboject we received - be carefull with that, as in my case it was almost 5000 lines.
package main
import (
"fmt"
"io/ioutil"
"log"
"os"
xfnv1alpha1 "github.com/crossplane/crossplane/apis/apiextensions/fn/io/v1alpha1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/yaml"
)
func main() {
funcIO := xfnv1alpha1.FunctionIO{}
x, err := ioutil.ReadAll(os.Stdin)
if err != nil {
log.Fatal(err)
}
// it's necessary to use sigs.k8s.io/yaml parser, otherwise it's impossible to force functions to work
err = yaml.Unmarshal(x, &funcIO)
if err != nil {
log.Fatal(err)
}
object := `
apiVersion: kubernetes.crossplane.io/v1alpha1
kind: Object
metadata:
name: testingconfigmaps
spec:
providerConfigRef:
name: kubernetes
forProvider:
manifest:
apiVersion: v1
kind: ConfigMap
metadata:
name: url-config
namespace: default
data:
fullURL: "https://google.pl"`
k8sapproved, err := yaml.YAMLToJSON([]byte(object))
if err != nil {
log.Fatal("from k8sapproved", err)
}
funcIO.Desired.Composite.Resource.Raw = funcIO.Observed.Composite.Resource.Raw
funcIO.Desired.Resources = append(funcIO.Desired.Resources, xfnv1alpha1.DesiredResource{
Name: "examplename",
Resource: runtime.RawExtension{
Raw: k8sapproved,
},
},
)
funcIO.Results = append(funcIO.Results,
xfnv1alpha1.Result{
Severity: xfnv1alpha1.SeverityNormal,
Message: fmt.Sprintf("Print whole object: \n\n\n%s\n\n\n", string(x)),
},
)
d1, err := yaml.Marshal(funcIO)
if err != nil {
log.Fatal(err)
}
// return new state to stdout
fmt.Println(string(d1))
}
Example Dockerfile to build container:
FROM golang:latest AS build
WORKDIR /build
COPY go.mod /build/
COPY go.sum /build/
RUN go mod download
COPY main.go /build/
RUN go build -o /build/fnio
FROM ubuntu:latest
COPY --from=build /build/fnio /fnio
ENTRYPOINT [ "/fnio" ]
Example 2 - Python
In my humble opinion Python is much better for Functions use case and I highly recommend using it. As a result of below code we will add new key:value pair into existing Secret. Resulting secret will contain new entry in stringData: POSTGRESQL_URL=postgresql://sally:sallyspassword@dbserver.example:5555/userdata?connect_timeout=10&sslmode=require&target_session_attrs=primary
import sys
import yaml
def read_Functionio() -> dict:
"""Read the FunctionIO from stdin."""
return yaml.load(sys.stdin.read(), yaml.Loader)
def write_Functionio(Functionio: dict):
"""Write the FunctionIO to stdout and exit."""
sys.stdout.write(yaml.dump(Functionio))
sys.exit(0)
def main():
fnio = read_Functionio()
connstring = 'postgresql://sally:sallyspassword@dbserver.example:5555/userdata?connect_timeout=10&sslmode=require&target_session_attrs=primary'
for elem in fnio['desired']['resources']:
if elem['name'] == 'mySecret':
elem['resource']['spec']['forProvider']['manifest']['stringData']['POSTGRESQL_URL'] = connstring
write_Functionio(fnio)
main()
Example Dockerfile to build container:
FROM python:3.9-slim-buster AS build
WORKDIR /build
COPY requirements.txt /build/requirements.txt
COPY main2.py /fnio
RUN pip3 install -r requirements.txt
ENTRYPOINT [ "python3", "/fnio" ]
Worth to know
-
All resources in Composition must be named if You plan to use Functions
- name: resource1
base:
apiVersion: kubernetes.crossplane.io/v1alpha1
(...)
-
debugging Functions is hard, it’s worth to print our FunctionIO once to log, save it and then operate on it locally
-
do not use
FROM scratch
in Dockerfile - it causes issues at least in version v1.11 -
original composition (fully printed) can we found here: github.com/wejdross/crossplanefunctions/blob/master/log.log as well as mentioned above examples