Skip to content

Add methods for type-aware functions, change errors and panics #632

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

Merged
merged 4 commits into from
Oct 7, 2022
Merged
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
2 changes: 1 addition & 1 deletion go/fn/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
Expand Down
23 changes: 11 additions & 12 deletions go/fn/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,17 @@
/*
Package fn provides the SDK to write KRM functions.

Before you start
# Before you start

This fn SDK requires some basic KRM function Specification knowledge. To make the best usage of your time, we recommend
you to be familiar with "ResourceList" before moving forward.

The KRM Function Specification, or "ResourceList", defines the standards of the inter-process communication between
the orchestrator (i.e. kpt CLI) and functions.
The KRM Function Specification, or "ResourceList", defines the standards of the inter-process communication between
the orchestrator (i.e. kpt CLI) and functions.

See KRM Function Specification reference in https://github.com/kubernetes-sigs/kustomize/blob/master/cmd/config/docs/api-conventions/functions-spec.md

KRM Function
# KRM Function

A KRM function can mutate and/or validate Kubernetes resources in a ResourceList.

Expand All @@ -37,13 +37,13 @@ Read more about how to use KRM functions in https://kpt.dev/book/04-using-functi
Read more about how to develop a KRM function in https://kpt.dev/book/05-developing-functions/

A general workflow is:
1. Reads the "ResourceList" object from STDIN.
2. Gets the function configs from the "ResourceList.FunctionConfig".
3. Mutate or validate the Kubernetes YAML resources from the "ResourceList.Items" field with the function configs.
4. Writes the modified "ResourceList" to STDOUT.
5. Write function message to "ResourceList.Results" with severity "Info", "Warning" or "Error"
1. Reads the "ResourceList" object from STDIN.
2. Gets the function configs from the "ResourceList.FunctionConfig".
3. Mutate or validate the Kubernetes YAML resources from the "ResourceList.Items" field with the function configs.
4. Writes the modified "ResourceList" to STDOUT.
5. Write function message to "ResourceList.Results" with severity "Info", "Warning" or "Error"

KubeObject
# KubeObject

The KubeObject is the basic unit to perform operations on KRM resources.

Expand All @@ -65,14 +65,13 @@ SubObject.NestedInt64("replicas")
Besides unstructured style, another way to use KubeObject is to purely work on the KubeObject/SubObject by calling
"GetMap", "GetSlice", "UpsertMap" which expects the return to be SubObject(s) pointer.

AsMain
# AsMain

"AsMain" is the main entrypoint. In most cases, you only need to provide the mutator or validation logic and have AsMain
handles the ResourceList parsing, KRM resource field type detection, read from STDIN and write to STDOUT.

"AsMain" accepts a struct that either implement the ResourceListProcessor interface or Runner interface.

See github.com/GoogleContainerTools/kpt-functions-sdk/go/fn/examples for detailed usage.

*/
package fn
56 changes: 24 additions & 32 deletions go/fn/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,45 +19,15 @@ import (
"strings"
)

const pathDelimitor = "."

// ErrMissingFnConfig raises error if a required functionConfig is missing.
type ErrMissingFnConfig struct{}

func (ErrMissingFnConfig) Error() string {
return "unable to find the functionConfig in the resourceList"
}

// errKubeObjectFields raises if the KubeObject operation panics.
type errKubeObjectFields struct {
obj *KubeObject
fields []string
}

func (e *errKubeObjectFields) Error() string {
return fmt.Sprintf("Resource(apiVersion=%v, kind=%v, Name=%v) has unmatched field type: `%v",
e.obj.GetAPIVersion(), e.obj.GetKind(), e.obj.GetName(), strings.Join(e.fields, "/"))
}

// errSubObjectFields raises if the SubObject operation panics.
type errSubObjectFields struct {
fields []string
}

func (e *errSubObjectFields) Error() string {
return fmt.Sprintf("SubObject has unmatched field type: `%v", strings.Join(e.fields, "/"))
}

type errResultEnd struct {
obj *KubeObject
message string
}

func (e *errResultEnd) Error() string {
if e.obj != nil {
return fmt.Sprintf("function is terminated by %v: %v", e.obj.ShortString(), e.message)
}
return fmt.Sprintf("function is terminated: %v", e.message)
}

type ErrAttemptToTouchUpstreamIdentifier struct{}

func (ErrAttemptToTouchUpstreamIdentifier) Error() string {
Expand All @@ -71,3 +41,25 @@ type ErrInternalAnnotation struct {
func (e *ErrInternalAnnotation) Error() string {
return e.Message
}

// NewErrUnmatchedField returns a ErrUnmatchedField error with the specific field path of a KubeObject that has the mismatched data type.
func NewErrUnmatchedField(obj SubObject, fields []string, expectedFieldType any) *ErrUnmatchedField {
relativefields := strings.Join(fields, pathDelimitor)
obj.fieldpath += pathDelimitor + relativefields
return &ErrUnmatchedField{
SubObject: &obj, DataType: fmt.Sprintf("%T", expectedFieldType),
}
}

// ErrUnmatchedField defines the error when a KubeObject's field paths has a different data type as expected
// e.g. ConfigMap `.data` is string map. If the a ConfigMap KubeObject calls `NestedInt("data")`, this error should raise.
type ErrUnmatchedField struct {
SubObject *SubObject
DataType string
}

// Error returns the message to guide users
func (e *ErrUnmatchedField) Error() string {
return fmt.Sprintf("Resource(apiVersion=%v, kind=%v) has unmatched field type %q in fieldpath %v",
e.SubObject.parentGVK.GroupVersion(), e.SubObject.parentGVK.Kind, e.DataType, e.SubObject.fieldpath)
}
29 changes: 19 additions & 10 deletions go/fn/examples/example_asmain_runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
Expand All @@ -30,14 +30,21 @@ type SetLabels struct {
// `ctx` provides easy methods to add info, error or warning result to `ResourceList.Results`.
// `items` is parsed from the STDIN "ResourceList.Items".
// `functionConfig` is from the STDIN "ResourceList.FunctionConfig". The value has been assigned to the r.Labels
// the functionConfig is validated to have kind "SetLabels" and apiVersion "fn.kpt.dev/v1alpha1"
//
// the functionConfig is validated to have kind "SetLabels" and apiVersion "fn.kpt.dev/v1alpha1"
func (r *SetLabels) Run(ctx *fn.Context, functionConfig *fn.KubeObject, items fn.KubeObjects, results *fn.Results) bool {
for _, o := range items {
for k, newLabel := range r.Labels {
o.SetLabel(k, newLabel)
err := o.SetLabel(k, newLabel)
if err != nil {
results.ErrorE(err)
// continue even if error occurs, we want the final results to show all errors.
}
}
}
results.Infof("updated labels")
if results.ExitCode() != 1 {
results.Infof("updated labels")
}
return true
}

Expand All @@ -50,13 +57,15 @@ func (r *SetLabels) Run(ctx *fn.Context, functionConfig *fn.KubeObject, items fn
// - apiVersion: v1
// kind: Service
// metadata:
// name: example
// name: example
//
// functionConfig:
// apiVersion: fn.kpt.dev/v1alpha1
// kind: SetLabels
// metadata:
// name: setlabel-fn-config
func main() {
//
// apiVersion: fn.kpt.dev/v1alpha1
// kind: SetLabels
// metadata:
// name: setlabel-fn-config
func Example_asMain() {
file, _ := os.Open("./data/setlabels-resourcelist.yaml")
defer file.Close()
os.Stdin = file
Expand Down
9 changes: 7 additions & 2 deletions go/fn/examples/example_filter_GVK_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,14 @@ func updateReplicas(rl *fn.ResourceList) (bool, error) {
return false, fn.ErrMissingFnConfig{}
}
var replicas int
rl.FunctionConfig.GetOrDie(&replicas, "replicas")
found, err := rl.FunctionConfig.NestedResource(&replicas, "replicas")
if err != nil || !found {
return found, err
}
for i := range rl.Items.Where(fn.IsGVK("apps", "v1", "Deployment")) {
rl.Items[i].SetOrDie(replicas, "spec", "replicas")
if err := rl.Items[i].SetNestedField(replicas, "spec", "replicas"); err != nil {
return false, err
}
}
return true, nil
}
4 changes: 2 additions & 2 deletions go/fn/examples/example_generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ func generate(rl *fn.ResourceList) (bool, error) {
return false, fn.ErrMissingFnConfig{}
}

revision := rl.FunctionConfig.NestedStringOrDie("data", "revision")
id := rl.FunctionConfig.NestedStringOrDie("data", "id")
revision, _, _ := rl.FunctionConfig.NestedString("data", "revision")
id, _, _ := rl.FunctionConfig.NestedString("data", "id")
js, err := fetchDashboard(revision, id)
if err != nil {
return false, fmt.Errorf("fetch dashboard: %v", err)
Expand Down
26 changes: 18 additions & 8 deletions go/fn/examples/example_kubeobject_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,24 @@ func Example_kubeObjectMutatePrimitiveField() {
replicas := spec.GetInt("replicas")
// mutate the replicas variable

spec.SetNestedIntOrDie(int(replicas))
err := spec.SetNestedInt(int(replicas))
if err != nil {
panic(err)
}
}

func Example_kubeObjectMutatePrimitiveSlice() {
finalizers := deployment.NestedStringSliceOrDie("metadata", "finalizers")
finalizers, _, _ := deployment.NestedStringSlice("metadata", "finalizers")
// mutate the finalizers slice

deployment.SetNestedStringSliceOrDie(finalizers, "metadata", "finalizers")
err := deployment.SetNestedStringSlice(finalizers, "metadata", "finalizers")
if err != nil {
panic(err)
}
}

func Example_kubeObjectMutatePrimitiveMap() {
data := configMap.NestedStringMapOrDie("data")
data, _, _ := configMap.NestedStringMap("data")
// mutate the data map

err := deployment.SetNestedStringMap(data, "data")
Expand All @@ -52,17 +58,21 @@ func Example_kubeObjectMutateStrongTypedField() {
var newPodTemplate corev1.PodTemplate
curPodTemplate := configMap.GetMap("spec").GetMap("template")
// Assign the current PodTemplate value to newPodTemplate
// Use AsOrDie to AsMain handles the errors.
curPodTemplate.AsOrDie(&newPodTemplate)
// Use As to AsMain handles the errors.
err := curPodTemplate.As(&newPodTemplate)
if err != nil {
panic(err)
}
// mutate the newPodTemplate object
err := deployment.SetNestedField(newPodTemplate, "spec", "template")
err = deployment.SetNestedField(newPodTemplate, "spec", "template")
if err != nil { /* do something */
panic(err)
}
}

func Example_kubeObjectMutateStrongTypedSlice() {
var containers []corev1.Container
found, err := deployment.Get(&containers, "spec", "template", "spec", "containers")
found, err := deployment.NestedResource(&containers, "spec", "template", "spec", "containers")
if err != nil { /* do something */
}
if !found { /* do something */
Expand Down
9 changes: 7 additions & 2 deletions go/fn/examples/example_logger_injector_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@ func injectLogger(rl *fn.ResourceList) (bool, error) {
}
for i, obj := range rl.Items.Where(hasDesiredGVK) {
var containers []corev1.Container
obj.GetOrDie(&containers, "spec", "template", "spec", "containers")
found, err := obj.NestedResource(&containers, "spec", "template", "spec", "containers")
if err != nil || !found {
return found, err
}
foundTargetContainer := false
for j, container := range containers {
if container.Name == li.ContainerName {
Expand All @@ -57,7 +60,9 @@ func injectLogger(rl *fn.ResourceList) (bool, error) {
}
containers = append(containers, c)
}
rl.Items[i].SetOrDie(containers, "spec", "template", "spec", "containers")
if err = rl.Items[i].SetNestedField(containers, "spec", "template", "spec", "containers"); err != nil {
return false, nil
}
}
return true, nil
}
Expand Down
14 changes: 9 additions & 5 deletions go/fn/examples/example_read_field_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,15 @@ func Example_aReadField() {
func readField(rl *fn.ResourceList) (bool, error) {
for _, obj := range rl.Items.Where(fn.IsGVK("apps", "v1", "Deployment")) {
// Style 1: like using unstrucuted.Unstructured, get/set the value from field paths*
replicas := obj.NestedInt64OrDie("spec", "replicas")
replicas, _, _ := obj.NestedInt64("spec", "replicas")
fn.Logf("replicas is %v\n", replicas)
paused := obj.NestedBoolOrDie("spec", "paused")
paused, _, _ := obj.NestedBool("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")
if strategy, _, _ := obj.NestedString("spec", "strategy", "type"); strategy == "Recreate" {
if err := obj.SetNestedString("RollingUpdate", "spec", "strategy", "type"); err != nil {
return false, err
}
}

// Style 2: operate each resource layer via `GetMap`
Expand All @@ -46,7 +48,9 @@ func readField(rl *fn.ResourceList) (bool, error) {
fn.Logf("replicas is %v\n", replicas)
nodeSelector := spec.GetMap("template").GetMap("spec").GetMap("nodeSelector")
if nodeSelector.GetString("disktype") != "ssd" {
nodeSelector.SetNestedStringOrDie("ssd", "disktype")
if err := nodeSelector.SetNestedString("ssd", "disktype"); err != nil {
return false, err
}
}
}
return true, nil
Expand Down
4 changes: 3 additions & 1 deletion go/fn/examples/example_read_functionConfig_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ func Example_bReadFunctionConfig() {

func readFunctionConfig(rl *fn.ResourceList) (bool, error) {
var sr SetReplicas
rl.FunctionConfig.AsOrDie(&sr)
if err := rl.FunctionConfig.As(&sr); err != nil {
return false, err
}
fn.Logf("desired replicas is %v\n", sr.DesiredReplicas)
return true, nil
}
Expand Down
4 changes: 3 additions & 1 deletion go/fn/examples/example_set_field_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ func setField(rl *fn.ResourceList) (bool, error) {
for _, obj := range rl.Items {
if obj.GetAPIVersion() == "apps/v1" && obj.GetKind() == "Deployment" {
replicas := 10
obj.SetOrDie(&replicas, "spec", "replicas")
if err := obj.SetNestedField(&replicas, "spec", "replicas"); err != nil {
return false, err
}
}
}
return true, nil
Expand Down
4 changes: 3 additions & 1 deletion go/fn/examples/example_validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ func validator(rl *fn.ResourceList) (bool, error) {
var results fn.Results
for _, obj := range rl.Items.Where(hasDesiredGVK) {
var runAsNonRoot bool
obj.GetOrDie(&runAsNonRoot, "spec", "template", "spec", "securityContext", "runAsNonRoot")
if _, err := obj.NestedResource(&runAsNonRoot, "spec", "template", "spec", "securityContext", "runAsNonRoot"); err != nil {
return false, err
}
if !runAsNonRoot {
results = append(results, fn.ConfigObjectResult("`spec.template.spec.securityContext.runAsNonRoot` must be set to true", obj, fn.Error))
}
Expand Down
7 changes: 3 additions & 4 deletions go/fn/examples/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,20 @@ require (
github.com/google/gofuzz v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kr/pretty v0.2.1 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/testify v1.7.1 // indirect
github.com/stretchr/testify v1.8.0 // indirect
github.com/xlab/treeprint v1.1.0 // indirect
golang.org/x/net v0.0.0-20220412020605-290c469a71a5 // indirect
golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect
golang.org/x/text v0.3.7 // indirect
google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/klog/v2 v2.60.1 // indirect
k8s.io/kube-openapi v0.0.0-20220401212409-b28bf2818661 // indirect
k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 // indirect
Expand Down
Loading