Models

Upbound Official Providers and some other packages include Go models: packages containing struct type definitions for their resources. These models enable in-line documentation, linting, autocompletion, and other features when working with Crossplane resources in embedded Go functions. It is also possible to use the Go types from the upstream provider source code with embedded Go functions.

Make models available to a function

Use up dependency add to make models from a dependency available to a function. Dependencies are most often Crossplane providers, but they can also be configurations that include XRDs.

up dependency add xpkg.upbound.io/upbound/provider-aws-s3:v1.20.0

Use up project build to make models available for XRDs defined by your project.

up project build
Tip
up caches Go models in the .up/go directory, at the root of your project. You shouldn’t commit the .up directory to source control.

Import models into a function

Each provider’s models are available in their own packages, named after the provider’s resource group and versions. The model packages are all contained in a single Go module with the path dev.upbound.io/models. Note that this module is always imported via a replace directive in the go.mod file and cannot otherwise be resolved by the Go tooling.

Import models to your fn.go function file with the following syntax:

import "dev.upbound.io/models/io/upbound/aws/s3/v1alpha1"

Use model in a function

Tip
Refer to the Crossplane Go SDK documentation for full details on the functions described below.

Once you import the model, you can convert import resources to model types and construct output resources using model types. The easiest way to convert between the Crossplane SDK’s types and model types is via JSON using a utility function like the following:

func convertViaJSON(to, from any) error {
	bs, err := json.Marshal(from)
	if err != nil {
		return err
	}
	return json.Unmarshal(bs, to)
}

Crossplane passes resources to your function as protocol buffer structs. Convert them to model types to take advantage of type checking, linting, and autocompletion:

package main

import (
	"context"
	"encoding/json"

	"dev.upbound.io/models/com/example/platform/v1alpha1"

	"github.com/crossplane/function-sdk-go/errors"
	"github.com/crossplane/function-sdk-go/logging"
	fnv1 "github.com/crossplane/function-sdk-go/proto/v1"
	"github.com/crossplane/function-sdk-go/request"
	"github.com/crossplane/function-sdk-go/response"
)

// Function is your composition function.
type Function struct {
	fnv1.UnimplementedFunctionRunnerServiceServer

	log logging.Logger
}

// RunFunction runs the Function.
func (f *Function) RunFunction(_ context.Context, req *fnv1.RunFunctionRequest) (*fnv1.RunFunctionResponse, error) {
	rsp := response.To(req, response.DefaultTTL)

	observedComposite, err := request.GetObservedCompositeResource(req)
	if err != nil {
		response.Fatal(rsp, errors.Wrap(err, "cannot get xr"))
		return rsp, nil
	}
	var xr v1alpha1.XStorageBucket
	if err := convertViaJSON(&xr, observedComposite.Resource); err != nil {
		response.Fatal(rsp, errors.Wrap(err, "cannot convert xr"))
		return rsp, nil
	}
    
	return rsp, nil
}

Use response.SetDesiredComposedResources to add composed resources to the function’s response:

package main

import (
	"context"
	"encoding/json"

	"dev.upbound.io/models/io/upbound/aws/s3/v1beta1"
	"k8s.io/utils/ptr"

	"github.com/crossplane/function-sdk-go/errors"
	"github.com/crossplane/function-sdk-go/logging"
	fnv1 "github.com/crossplane/function-sdk-go/proto/v1"
	"github.com/crossplane/function-sdk-go/request"
	"github.com/crossplane/function-sdk-go/resource"
	"github.com/crossplane/function-sdk-go/resource/composed"
	"github.com/crossplane/function-sdk-go/response"
)

// Function is your composition function.
type Function struct {
	fnv1.UnimplementedFunctionRunnerServiceServer

	log logging.Logger
}

// RunFunction runs the Function.
func (f *Function) RunFunction(_ context.Context, req *fnv1.RunFunctionRequest) (*fnv1.RunFunctionResponse, error) {
	rsp := response.To(req, response.DefaultTTL)

	bucket := &v1beta1.Bucket{
		APIVersion: ptr.To("s3.aws.upbound.io/v1beta1"),
		Kind:       ptr.To("Bucket"),
		Spec: &v1beta1.BucketSpec{
			ForProvider: &v1beta1.BucketSpecForProvider{
				Region: ptr.To("us-east-1"),
			},
		},
	}

	desiredComposedResources, err := request.GetDesiredComposedResources(req)
	if err != nil {
		response.Fatal(rsp, errors.Wrap(err, "cannot get desired resources"))
		return rsp, nil
	}

	c := composed.New()
	if err := convertViaJSON(c, bucket); err != nil {
		response.Fatal(rsp, errors.Wrap(err, "cannot convert bucket to unstructured"))
		return rsp, nil
	}
	desiredComposedResources["bucket"] = &resource.DesiredComposed{Resource: c}

	if err := response.SetDesiredComposedResources(rsp, desiredComposedResources); err != nil {
		response.Fatal(rsp, errors.Wrap(err, "cannot set desired resources"))
		return rsp, nil
	}

	return rsp, nil
}

Supported packages

All Upbound Official Providers include Go models.

When you build your project with up project build, the generated artifact contains the generated models for your XRDs. You can build a project and then import that project as a dependency for the resources you define. You can also use your own project’s models in your functions as described above.

Field types in models

All fields in Upbound’s Go models have pointer types so that you can specify only the fields your function has an opinion about. This can be awkward in Go, since there’s no built-in way to construct a pointer to a constant value. The k8s.io/utils/ptr package contains a function, ptr.To, which can be used for this purpose:

package main

import (
	"context"
	"encoding/json"

	"dev.upbound.io/models/io/upbound/aws/s3/v1beta1"
	"k8s.io/utils/ptr"

	"github.com/crossplane/function-sdk-go/logging"
	fnv1 "github.com/crossplane/function-sdk-go/proto/v1"
	"github.com/crossplane/function-sdk-go/response"
)

// Function is your composition function.
type Function struct {
	fnv1.UnimplementedFunctionRunnerServiceServer

	log logging.Logger
}

// RunFunction runs the Function.
func (f *Function) RunFunction(_ context.Context, req *fnv1.RunFunctionRequest) (*fnv1.RunFunctionResponse, error) {
	f.log.Info("Running function", "tag", req.GetMeta().GetTag())
	rsp := response.To(req, response.DefaultTTL)

	bucket := &v1beta1.Bucket{
		APIVersion: ptr.To("s3.aws.upbound.io/v1beta1"),
		Kind:       ptr.To("Bucket"),
		Spec: &v1beta1.BucketSpec{
			ForProvider: &v1beta1.BucketSpecForProvider{
				Region: ptr.To("us-east-1"),
			},
		},
	}
    
	return rsp, nil
}