Skip to content

Commit 5fa523b

Browse files
authored
Add methods for type-aware functions, change errors and panics (#632)
1 parent 3435179 commit 5fa523b

26 files changed

+363
-485
lines changed

go/fn/const.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
// you may not use this file except in compliance with the License.
55
// You may obtain a copy of the License at
66
//
7-
// http://www.apache.org/licenses/LICENSE-2.0
7+
// http://www.apache.org/licenses/LICENSE-2.0
88
//
99
// Unless required by applicable law or agreed to in writing, software
1010
// distributed under the License is distributed on an "AS IS" BASIS,

go/fn/doc.go

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,17 @@
1515
/*
1616
Package fn provides the SDK to write KRM functions.
1717
18-
Before you start
18+
# Before you start
1919
2020
This fn SDK requires some basic KRM function Specification knowledge. To make the best usage of your time, we recommend
2121
you to be familiar with "ResourceList" before moving forward.
2222
23-
The KRM Function Specification, or "ResourceList", defines the standards of the inter-process communication between
24-
the orchestrator (i.e. kpt CLI) and functions.
23+
The KRM Function Specification, or "ResourceList", defines the standards of the inter-process communication between
24+
the orchestrator (i.e. kpt CLI) and functions.
2525
2626
See KRM Function Specification reference in https://github.com/kubernetes-sigs/kustomize/blob/master/cmd/config/docs/api-conventions/functions-spec.md
2727
28-
KRM Function
28+
# KRM Function
2929
3030
A KRM function can mutate and/or validate Kubernetes resources in a ResourceList.
3131
@@ -37,13 +37,13 @@ Read more about how to use KRM functions in https://kpt.dev/book/04-using-functi
3737
Read more about how to develop a KRM function in https://kpt.dev/book/05-developing-functions/
3838
3939
A general workflow is:
40-
1. Reads the "ResourceList" object from STDIN.
41-
2. Gets the function configs from the "ResourceList.FunctionConfig".
42-
3. Mutate or validate the Kubernetes YAML resources from the "ResourceList.Items" field with the function configs.
43-
4. Writes the modified "ResourceList" to STDOUT.
44-
5. Write function message to "ResourceList.Results" with severity "Info", "Warning" or "Error"
40+
1. Reads the "ResourceList" object from STDIN.
41+
2. Gets the function configs from the "ResourceList.FunctionConfig".
42+
3. Mutate or validate the Kubernetes YAML resources from the "ResourceList.Items" field with the function configs.
43+
4. Writes the modified "ResourceList" to STDOUT.
44+
5. Write function message to "ResourceList.Results" with severity "Info", "Warning" or "Error"
4545
46-
KubeObject
46+
# KubeObject
4747
4848
The KubeObject is the basic unit to perform operations on KRM resources.
4949
@@ -65,14 +65,13 @@ SubObject.NestedInt64("replicas")
6565
Besides unstructured style, another way to use KubeObject is to purely work on the KubeObject/SubObject by calling
6666
"GetMap", "GetSlice", "UpsertMap" which expects the return to be SubObject(s) pointer.
6767
68-
AsMain
68+
# AsMain
6969
7070
"AsMain" is the main entrypoint. In most cases, you only need to provide the mutator or validation logic and have AsMain
7171
handles the ResourceList parsing, KRM resource field type detection, read from STDIN and write to STDOUT.
7272
7373
"AsMain" accepts a struct that either implement the ResourceListProcessor interface or Runner interface.
7474
7575
See github.com/GoogleContainerTools/kpt-functions-sdk/go/fn/examples for detailed usage.
76-
7776
*/
7877
package fn

go/fn/errors.go

Lines changed: 24 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -19,45 +19,15 @@ import (
1919
"strings"
2020
)
2121

22+
const pathDelimitor = "."
23+
2224
// ErrMissingFnConfig raises error if a required functionConfig is missing.
2325
type ErrMissingFnConfig struct{}
2426

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

29-
// errKubeObjectFields raises if the KubeObject operation panics.
30-
type errKubeObjectFields struct {
31-
obj *KubeObject
32-
fields []string
33-
}
34-
35-
func (e *errKubeObjectFields) Error() string {
36-
return fmt.Sprintf("Resource(apiVersion=%v, kind=%v, Name=%v) has unmatched field type: `%v",
37-
e.obj.GetAPIVersion(), e.obj.GetKind(), e.obj.GetName(), strings.Join(e.fields, "/"))
38-
}
39-
40-
// errSubObjectFields raises if the SubObject operation panics.
41-
type errSubObjectFields struct {
42-
fields []string
43-
}
44-
45-
func (e *errSubObjectFields) Error() string {
46-
return fmt.Sprintf("SubObject has unmatched field type: `%v", strings.Join(e.fields, "/"))
47-
}
48-
49-
type errResultEnd struct {
50-
obj *KubeObject
51-
message string
52-
}
53-
54-
func (e *errResultEnd) Error() string {
55-
if e.obj != nil {
56-
return fmt.Sprintf("function is terminated by %v: %v", e.obj.ShortString(), e.message)
57-
}
58-
return fmt.Sprintf("function is terminated: %v", e.message)
59-
}
60-
6131
type ErrAttemptToTouchUpstreamIdentifier struct{}
6232

6333
func (ErrAttemptToTouchUpstreamIdentifier) Error() string {
@@ -71,3 +41,25 @@ type ErrInternalAnnotation struct {
7141
func (e *ErrInternalAnnotation) Error() string {
7242
return e.Message
7343
}
44+
45+
// NewErrUnmatchedField returns a ErrUnmatchedField error with the specific field path of a KubeObject that has the mismatched data type.
46+
func NewErrUnmatchedField(obj SubObject, fields []string, expectedFieldType any) *ErrUnmatchedField {
47+
relativefields := strings.Join(fields, pathDelimitor)
48+
obj.fieldpath += pathDelimitor + relativefields
49+
return &ErrUnmatchedField{
50+
SubObject: &obj, DataType: fmt.Sprintf("%T", expectedFieldType),
51+
}
52+
}
53+
54+
// ErrUnmatchedField defines the error when a KubeObject's field paths has a different data type as expected
55+
// e.g. ConfigMap `.data` is string map. If the a ConfigMap KubeObject calls `NestedInt("data")`, this error should raise.
56+
type ErrUnmatchedField struct {
57+
SubObject *SubObject
58+
DataType string
59+
}
60+
61+
// Error returns the message to guide users
62+
func (e *ErrUnmatchedField) Error() string {
63+
return fmt.Sprintf("Resource(apiVersion=%v, kind=%v) has unmatched field type %q in fieldpath %v",
64+
e.SubObject.parentGVK.GroupVersion(), e.SubObject.parentGVK.Kind, e.DataType, e.SubObject.fieldpath)
65+
}

go/fn/examples/example_asmain_runner_test.go

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
// you may not use this file except in compliance with the License.
55
// You may obtain a copy of the License at
66
//
7-
// http://www.apache.org/licenses/LICENSE-2.0
7+
// http://www.apache.org/licenses/LICENSE-2.0
88
//
99
// Unless required by applicable law or agreed to in writing, software
1010
// distributed under the License is distributed on an "AS IS" BASIS,
@@ -30,14 +30,21 @@ type SetLabels struct {
3030
// `ctx` provides easy methods to add info, error or warning result to `ResourceList.Results`.
3131
// `items` is parsed from the STDIN "ResourceList.Items".
3232
// `functionConfig` is from the STDIN "ResourceList.FunctionConfig". The value has been assigned to the r.Labels
33-
// the functionConfig is validated to have kind "SetLabels" and apiVersion "fn.kpt.dev/v1alpha1"
33+
//
34+
// the functionConfig is validated to have kind "SetLabels" and apiVersion "fn.kpt.dev/v1alpha1"
3435
func (r *SetLabels) Run(ctx *fn.Context, functionConfig *fn.KubeObject, items fn.KubeObjects, results *fn.Results) bool {
3536
for _, o := range items {
3637
for k, newLabel := range r.Labels {
37-
o.SetLabel(k, newLabel)
38+
err := o.SetLabel(k, newLabel)
39+
if err != nil {
40+
results.ErrorE(err)
41+
// continue even if error occurs, we want the final results to show all errors.
42+
}
3843
}
3944
}
40-
results.Infof("updated labels")
45+
if results.ExitCode() != 1 {
46+
results.Infof("updated labels")
47+
}
4148
return true
4249
}
4350

@@ -50,13 +57,15 @@ func (r *SetLabels) Run(ctx *fn.Context, functionConfig *fn.KubeObject, items fn
5057
// - apiVersion: v1
5158
// kind: Service
5259
// metadata:
53-
// name: example
60+
// name: example
61+
//
5462
// functionConfig:
55-
// apiVersion: fn.kpt.dev/v1alpha1
56-
// kind: SetLabels
57-
// metadata:
58-
// name: setlabel-fn-config
59-
func main() {
63+
//
64+
// apiVersion: fn.kpt.dev/v1alpha1
65+
// kind: SetLabels
66+
// metadata:
67+
// name: setlabel-fn-config
68+
func Example_asMain() {
6069
file, _ := os.Open("./data/setlabels-resourcelist.yaml")
6170
defer file.Close()
6271
os.Stdin = file

go/fn/examples/example_filter_GVK_test.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,14 @@ func updateReplicas(rl *fn.ResourceList) (bool, error) {
3434
return false, fn.ErrMissingFnConfig{}
3535
}
3636
var replicas int
37-
rl.FunctionConfig.GetOrDie(&replicas, "replicas")
37+
found, err := rl.FunctionConfig.NestedResource(&replicas, "replicas")
38+
if err != nil || !found {
39+
return found, err
40+
}
3841
for i := range rl.Items.Where(fn.IsGVK("apps", "v1", "Deployment")) {
39-
rl.Items[i].SetOrDie(replicas, "spec", "replicas")
42+
if err := rl.Items[i].SetNestedField(replicas, "spec", "replicas"); err != nil {
43+
return false, err
44+
}
4045
}
4146
return true, nil
4247
}

go/fn/examples/example_generator_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ func generate(rl *fn.ResourceList) (bool, error) {
4141
return false, fn.ErrMissingFnConfig{}
4242
}
4343

44-
revision := rl.FunctionConfig.NestedStringOrDie("data", "revision")
45-
id := rl.FunctionConfig.NestedStringOrDie("data", "id")
44+
revision, _, _ := rl.FunctionConfig.NestedString("data", "revision")
45+
id, _, _ := rl.FunctionConfig.NestedString("data", "id")
4646
js, err := fetchDashboard(revision, id)
4747
if err != nil {
4848
return false, fmt.Errorf("fetch dashboard: %v", err)

go/fn/examples/example_kubeobject_test.go

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,18 +29,24 @@ func Example_kubeObjectMutatePrimitiveField() {
2929
replicas := spec.GetInt("replicas")
3030
// mutate the replicas variable
3131

32-
spec.SetNestedIntOrDie(int(replicas))
32+
err := spec.SetNestedInt(int(replicas))
33+
if err != nil {
34+
panic(err)
35+
}
3336
}
3437

3538
func Example_kubeObjectMutatePrimitiveSlice() {
36-
finalizers := deployment.NestedStringSliceOrDie("metadata", "finalizers")
39+
finalizers, _, _ := deployment.NestedStringSlice("metadata", "finalizers")
3740
// mutate the finalizers slice
3841

39-
deployment.SetNestedStringSliceOrDie(finalizers, "metadata", "finalizers")
42+
err := deployment.SetNestedStringSlice(finalizers, "metadata", "finalizers")
43+
if err != nil {
44+
panic(err)
45+
}
4046
}
4147

4248
func Example_kubeObjectMutatePrimitiveMap() {
43-
data := configMap.NestedStringMapOrDie("data")
49+
data, _, _ := configMap.NestedStringMap("data")
4450
// mutate the data map
4551

4652
err := deployment.SetNestedStringMap(data, "data")
@@ -52,17 +58,21 @@ func Example_kubeObjectMutateStrongTypedField() {
5258
var newPodTemplate corev1.PodTemplate
5359
curPodTemplate := configMap.GetMap("spec").GetMap("template")
5460
// Assign the current PodTemplate value to newPodTemplate
55-
// Use AsOrDie to AsMain handles the errors.
56-
curPodTemplate.AsOrDie(&newPodTemplate)
61+
// Use As to AsMain handles the errors.
62+
err := curPodTemplate.As(&newPodTemplate)
63+
if err != nil {
64+
panic(err)
65+
}
5766
// mutate the newPodTemplate object
58-
err := deployment.SetNestedField(newPodTemplate, "spec", "template")
67+
err = deployment.SetNestedField(newPodTemplate, "spec", "template")
5968
if err != nil { /* do something */
69+
panic(err)
6070
}
6171
}
6272

6373
func Example_kubeObjectMutateStrongTypedSlice() {
6474
var containers []corev1.Container
65-
found, err := deployment.Get(&containers, "spec", "template", "spec", "containers")
75+
found, err := deployment.NestedResource(&containers, "spec", "template", "spec", "containers")
6676
if err != nil { /* do something */
6777
}
6878
if !found { /* do something */

go/fn/examples/example_logger_injector_test.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@ func injectLogger(rl *fn.ResourceList) (bool, error) {
4141
}
4242
for i, obj := range rl.Items.Where(hasDesiredGVK) {
4343
var containers []corev1.Container
44-
obj.GetOrDie(&containers, "spec", "template", "spec", "containers")
44+
found, err := obj.NestedResource(&containers, "spec", "template", "spec", "containers")
45+
if err != nil || !found {
46+
return found, err
47+
}
4548
foundTargetContainer := false
4649
for j, container := range containers {
4750
if container.Name == li.ContainerName {
@@ -57,7 +60,9 @@ func injectLogger(rl *fn.ResourceList) (bool, error) {
5760
}
5861
containers = append(containers, c)
5962
}
60-
rl.Items[i].SetOrDie(containers, "spec", "template", "spec", "containers")
63+
if err = rl.Items[i].SetNestedField(containers, "spec", "template", "spec", "containers"); err != nil {
64+
return false, nil
65+
}
6166
}
6267
return true, nil
6368
}

go/fn/examples/example_read_field_test.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,15 @@ func Example_aReadField() {
3131
func readField(rl *fn.ResourceList) (bool, error) {
3232
for _, obj := range rl.Items.Where(fn.IsGVK("apps", "v1", "Deployment")) {
3333
// Style 1: like using unstrucuted.Unstructured, get/set the value from field paths*
34-
replicas := obj.NestedInt64OrDie("spec", "replicas")
34+
replicas, _, _ := obj.NestedInt64("spec", "replicas")
3535
fn.Logf("replicas is %v\n", replicas)
36-
paused := obj.NestedBoolOrDie("spec", "paused")
36+
paused, _, _ := obj.NestedBool("spec", "paused")
3737
fn.Logf("paused is %v\n", paused)
3838
// Update strategy from Recreate to RollingUpdate.
39-
if strategy := obj.NestedStringOrDie("spec", "strategy", "type"); strategy == "Recreate" {
40-
obj.SetNestedStringOrDie("RollingUpdate", "spec", "strategy", "type")
39+
if strategy, _, _ := obj.NestedString("spec", "strategy", "type"); strategy == "Recreate" {
40+
if err := obj.SetNestedString("RollingUpdate", "spec", "strategy", "type"); err != nil {
41+
return false, err
42+
}
4143
}
4244

4345
// Style 2: operate each resource layer via `GetMap`
@@ -46,7 +48,9 @@ func readField(rl *fn.ResourceList) (bool, error) {
4648
fn.Logf("replicas is %v\n", replicas)
4749
nodeSelector := spec.GetMap("template").GetMap("spec").GetMap("nodeSelector")
4850
if nodeSelector.GetString("disktype") != "ssd" {
49-
nodeSelector.SetNestedStringOrDie("ssd", "disktype")
51+
if err := nodeSelector.SetNestedString("ssd", "disktype"); err != nil {
52+
return false, err
53+
}
5054
}
5155
}
5256
return true, nil

go/fn/examples/example_read_functionConfig_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ func Example_bReadFunctionConfig() {
3232

3333
func readFunctionConfig(rl *fn.ResourceList) (bool, error) {
3434
var sr SetReplicas
35-
rl.FunctionConfig.AsOrDie(&sr)
35+
if err := rl.FunctionConfig.As(&sr); err != nil {
36+
return false, err
37+
}
3638
fn.Logf("desired replicas is %v\n", sr.DesiredReplicas)
3739
return true, nil
3840
}

0 commit comments

Comments
 (0)