Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Function SDK guide #3367

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions scripts/generate_site_sidebar/sidebar_template.md.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -68,5 +68,6 @@
- [Namespace Provisioning UI](guides/namespace-provisioning-ui.md)
- [Variant Constructor Pattern](guides/variant-constructor-pattern.md)
- [Value Propagation Pattern](guides/value-propagation.md)
- [Effective Go KRM functions](guides/effective-go-krm-function.md)
- [FAQ](faq/)
- [Contact](contact/)
6 changes: 5 additions & 1 deletion site/book/05-developing-functions/02-developing-in-Go.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ You can develop a KRM function in Go using [the kpt function SDK].
In this quickstart, we will write a function that adds an annotation
`config.kubernetes.io/managed-by=kpt` to all `Deployment` resources.
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: config.kubernetes.io/managed-by:kpt ( s/=/:)


This quickstart takes 15 minutes to finish.

### Initialize your project

We start from a "get-started" package which contains a `main.go` file with some scaffolding code.
Expand Down Expand Up @@ -126,11 +128,13 @@ kpt fn eval ./data --image ${FN_CONTAINER_REGISTRY}/${FUNCTION_NAME}:${TAG}

## Next Steps

- See other [go doc examples] to use KubeObject.
- Read [effective go KRM function]
- See the [go doc examples] to use KubeObject.
- To contribute to KRM catalog functions, please follow the [contributor guide](https://github.com/GoogleContainerTools/kpt-functions-catalog/blob/master/CONTRIBUTING.md)

[the kpt function SDK]: https://pkg.go.dev/github.com/GoogleContainerTools/kpt-functions-sdk/go/fn
[go doc examples]: https://pkg.go.dev/github.com/GoogleContainerTools/kpt-functions-sdk/go/fn/examples
[`fn`]: https://pkg.go.dev/github.com/GoogleContainerTools/kpt-functions-sdk/go/fn
[`ResourceList`]: https://github.com/kubernetes-sigs/kustomize/blob/master/cmd/config/docs/api-conventions/functions-spec.md
[`unstructured.Unstrucutred`]: https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured
[effective go KRM function]: guides/effective-go-krm-function.md
3 changes: 2 additions & 1 deletion site/guides/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@
- [Namespace Provisioning CLI](guides/namespace-provisioning-cli.md)
- [Namespace Provisioning UI](guides/namespace-provisioning-ui.md)
- [Variant Constructor Pattern](guides/variant-constructor-pattern.md)
- [Value Propagation Pattern](guides/value-propagation.md)
- [Value Propagation Pattern](guides/value-propagation.md)
- [Effective Go KRM functions](guides/effective-go-krm-function.md)
212 changes: 212 additions & 0 deletions site/guides/effective-go-krm-function.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
# Effective GO KRM functions

This guide gives tips to effectively write a KRM function.

This guide is for advanced kpt function users who find the [catalog.kpt.dev] cannot fulfil their needs
and want to design their own KRM functions.
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 advanced kpt users can confuse users and might discourage users to read further.

Maybe we can combine this line and previous line with something along the lines of ...

This guide describes best practices and patterns on how to structure your Go KRM functions.


Suggest reading [Developing in Go] first
Copy link
Contributor

Choose a reason for hiding this comment

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

nit . missing at the end.


## Prerequisites

- [Install kpt]
- [Install Docker]
Copy link
Contributor

Choose a reason for hiding this comment

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

It will be good to specify minimum version of Go. I think Go 1.17+ is what we have been using for the toolset.


## Setup

<!--- TODO: Use scaffolding to generate the get-started package --->

We start from a "get-started-runner" package which contains a `main.go` file with some scaffolding code.


```shell
# Set your KRM function name.
export FUNCTION_NAME=<YOUR FUNCTION NAME>
export GOPATH=$(go env GOPATH)
export FUNCTION_PATH=github.com/<YOUR USERNAME>

# Create and direct to your Go working directory
mkdir -p $GOPATH/src/${FUNCTION_PATH} && cd $GOPATH/src/${FUNCTION_PATH}

# Get the "get-started" package.
kpt pkg get https://github.com/GoogleContainerTools/kpt-functions-sdk.git/go/get-started-runner@master ${FUNCTION_NAME}
Copy link
Contributor

Choose a reason for hiding this comment

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

This line didn't work for me ...

Error: path "/go/get-started-runner" does not exist in repo "https://github.com/GoogleContainerTools/kpt-functions-sdk"

Do I need to merge a PR in kpt-functions-sdk?

Copy link
Contributor

Choose a reason for hiding this comment

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

Assuming myself as a Go developer, I will find this step to be awkward :) and I will also be surprised to see files such as Kptfile etc. It might get confusing. Can we just use git clone or something to fetch the skeleton code ?


cd ${FUNCTION_NAME}

# Initialize Go module and install the kpt KRM function SDK.
go mod init && go mod tidy -compat=1.17
```

## Write your KRM function code in Go

In the main.go, you should have
```go
var _ fn.Runner = &FunctionX{}

type FunctionX struct {
// TODO: Modify with your expected function config.
FnConfigBool bool
FnConfigInt int
FnConfigFoo string
}

func (r *FunctionX) Run(ctx *fn.Context, functionConfig *fn.KubeObject, items fn.KubeObjects) {
// TODO: Add your KRM resource mutate or validate logic.
}

func main() {
if err := fn.AsMain(&FunctionX{}); err != nil {
os.Exit(1)
}
}
```
`FunctionX` implements the [`Runner`] interface that can process the input KRM resources as [`fn.ResourceList`], it initializes `fn.KubeObject` to hold the KRM resources,
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to mention ResourceList?

so that you can use [`fn.KubeObject` and `fn.SubObject`] methods directly. After `Run`, it will convert the modified `fn.KubeObjects` to KRM resources.
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of "it initializes fn.KubeObject to hold the KRM resources, so that you can use [fn.KubeObject and fn.SubObject] methods directly", I would just say that something like "the objects passed to the function are available in items. These objects can be modified in place and are used as the output of the function".


### Define configures
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe Define function parameters or Define function arguments?


If you need to use configurable variables, you can define them as `FunctionX` fields.
Otherwise you can skip this step and move to next.
```go
type FunctionX struct {
FnConfigBool bool
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe SomeBoolValue, SomeIntValue, SomeStringValue instead of the FnConfig prefix - otherwise the question is whether the FnConfig prefix matters

FnConfigInt int
FnConfigFoo string
}
```

For example, define a `SetImage` and add two variables to compare-and-swap the image value will be like:
```go
type SetImage struct {
OldImage string // Existing image
Copy link
Contributor

Choose a reason for hiding this comment

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

This is good - the example makes it much clearer. We might consider just walking through SetImage, instead of FunctionX. Start with no parameters, and then say "we want to change an image from an old image value to a new image value, so we need parameters so the user can specify the old image and the new image" or something like that

NewImage string // New image to replace
}
```

The `SetImage` is a KRM resource. It should be passed from the input as:
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe "This automatically defines a SetImage KRM resource. Users can pass it like this:" and then show a Kptfile or kpt package example, rather than ResourceList.

```yaml
apiVersion: config.kubernetes.io/v1
kind: ResourceList
functionConfig:
apiVersion: fn.kpt.dev/v1alpha1
# Kind is required to match the Runner struct
kind: SetImage
metadata:
name: try-out
oldImage: example
newImage: <YOUR NEW IMAGE NAME>
items:
...
```
<!--- TODO: we should not require users to understand and provide the input ResourceList.
We should build a test infra that users only provide the KRM resources--->
Copy link
Contributor

Choose a reason for hiding this comment

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

Agreed :-)


## Write the main logic in `Run`

The SDK will initialize a slice of `*fn.KubeObject` to hold your KRM resources. You will need to pass the
KRM resources from the input in `items` fields.
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 we've said this already?

```go
func (r *FunctionX) Run(ctx *fn.Context, functionConfig *fn.KubeObject, items fn.KubeObjects) {
// TODO: Add your KRM resource mutate or validate logic.
}

func main() {
if err := fn.AsMain(&FunctionX{}); err != nil {
os.Exit(1)
}
}
```

<!--- TODO: we should not require users to understand and provide the input ResourceList.
We should build a test infra that users only provide the KRM resources--->

### Select KRM resources
The `fn.KubeObjects` is a slice of `*fn.KubeObject`, that you can apply some select logic to easily choose
the target KRM resources. See below example on using `Where` and `WhereNot` to filter different types of resources.
```go
func (r *YourFunction) Run(context *fn.Context, functionConfig *fn.KubeObject, items fn.KubeObjects) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: if these are just examples, don't wrap them in a Run function ... otherwise users will think we're actually doing something here....

// namespaceScoped contains only namespace scoped resources
namespaceScoped := objects.Where(func(o *fn.KubeObject) bool { return o.IsNamespaceScoped() })
// clusterScoped contains only cluster scoped resources
clusterScoped := objects.Where(func(o *fn.KubeObject) bool { return o.IsClusterScoped() })
// customDeployment contains all resources of Kind "CustomDeployment", in Group "fn.kpt.dev" with any Versions.
customDeployment := objects.Where(fn.IsGVK("fn.kpt.dev", "", "CustomDeployment") })
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: I would support IsGK instead of IsGVK with a magic empty value

// excluded contains all resources except namespace objects
excluded := objects.WhereNot(fn.IsGVK("v1", "", "Namespace") })
}
```

### Read and write a field spec path of a KRM resource

Like [unstructured.Unstructured], `fn.KubeObject` (and `fn.SubObject`) provides a series of methods to
let you read and write different resources types.

```go
func (r *YourFunction) Run(context *fn.Context, functionConfig *fn.KubeObject, items fn.KubeObjects) {
// Get first deployment object.
deployment := items.Where(fn.IsGVK("apps", "v1", "Deployment")).Where(func(o *fn.KubeObject) bool{return o.GetName() == "nginx"})[0]
Copy link
Contributor

Choose a reason for hiding this comment

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

We probably want HasName or IsName, but OTOH it is helpful to spell this out.

You could spell it out, and then say "you can also use the IsName helper method, which produces the same filter function"

// Get the int value from deployment `spec.replicas`
replicas := deployment.NestedInt64OrDie("spec", "replicas")
fn.Logf("replicas is %v\n", replicas)
// Get the boolean value from deployment `spec.paused`
paused := deployment.NestedBoolOrDie("spec", "paused")
fn.Logf("paused is %v\n", paused)
// Update strategy from Recreate to RollingUpdate.
if strategy := obj.NestedStringOrDie("spec", "strategy", "type"); strategy == "Recreate" {
obj.SetNestedStringOrDie("RollingUpdate", "spec", "strategy", "type")
}
}
```

Besides the [unstructured.Unstructured] style, you can also run functions on each sub-field as a `fn.SubObject`
```go
func (r *YourFunction) Run(context *fn.Context, functionConfig *fn.KubeObject, items fn.KubeObjects) {
// Get first deployment object.
deployment := items.Where(fn.IsGVK("apps", "v1", "Deployment")).Where(func(o *fn.KubeObject) bool{return o.GetName() == "nginx"})[0]
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's okay to have this expression in multiple lines that helps with readability.

// Get a spec as a `SubObject`
spec := deployment.GetMap("spec")
// Get integer from SubObject spec
replicas = spec.GetInt("replicas")
fn.Logf("replicas is %v\n", replicas)
// Get the SubObject from another SubObject
nodeSelector := spec.GetMap("template").GetMap("spec").GetMap("nodeSelector")
if nodeSelector.GetString("disktype") != "ssd" {
nodeSelector.SetNestedStringOrDie("ssd", "disktype")
}
}
```

### Copy `KubeObject` to a typed struct

If you already have some struct to define a KRM resource (like `corev1.ConfigMap`), you can switch the `KubeObject`
to the other type via `As`
Copy link
Contributor

Choose a reason for hiding this comment

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

I would add an explicit warning that this copies the object and thus you have to copy it back

```go
func (r *YourFunction) Run(context *fn.Context, functionConfig *fn.KubeObject, items fn.KubeObjects) {
deploymentObject := objects.WhereNot(fn.IsGVK("apps", "v1", "Deployment") })[0]
deploymentSpec := deploymentObject.GetMap("spec")
var dpSpec appsv1.DeploymentSpec
deploymentSpec.As(&dpSpec)
dpSpec.Size()
}
```

<!-- TODO: We need a "Test the KRM function" section, which reuqires the SDK to provide the test infra so users only provide the input resource and expected output in YAML--->

[Install kpt]:
https://kpt.dev/installation/
[Install Docker]:
https://docs.docker.com/get-docker/
[`fn.ResourceList`]:
https://pkg.go.dev/github.com/GoogleContainerTools/kpt-functions-sdk/go/fn#ResourceList
[`fn.KubeObject` and `fn.SubObject`]:
https://pkg.go.dev/github.com/GoogleContainerTools/kpt-functions-sdk/go/fn#KubeObject
[Golang]:
https://go.dev/doc/gopath_code
[`Runner`]:
https://pkg.go.dev/github.com/GoogleContainerTools/kpt-functions-sdk/go/fn#Runner
[unstructured.Unstructured]:
https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured
[Developing in Go]:
https://kpt.dev/book/05-developing-functions/02-developing-in-Go
[catalog.kpt.dev]:
https://catalog.kpt.dev/
1 change: 1 addition & 0 deletions site/sidebar.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,5 +107,6 @@
- [Namespace Provisioning UI](guides/namespace-provisioning-ui.md)
- [Variant Constructor Pattern](guides/variant-constructor-pattern.md)
- [Value Propagation Pattern](guides/value-propagation.md)
- [Effective Go KRM functions](guides/effective-go-krm-function.md)
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it makes sense to have it alongside Getting Started instead of guides ?

- [FAQ](faq/)
- [Contact](contact/)