Skip to content

Add Results method and Context #631

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
26 changes: 26 additions & 0 deletions go/fn/context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// 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
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package fn

import (
"context"
)

var _ context.Context = &Context{}

// TODO: Have Context implement `context.Context`.
type Context struct {
context.Context
}
12 changes: 7 additions & 5 deletions go/fn/examples/example_asmain_runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
package example

import (
"context"
"os"

"github.com/GoogleContainerTools/kpt-functions-sdk/go/fn"
Expand All @@ -30,13 +31,14 @@ type SetLabels struct {
// `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"
func (r *SetLabels) Run(ctx *fn.Context, functionConfig *fn.KubeObject, items fn.KubeObjects) {
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)
}
}
ctx.ResultInfo("updated labels", nil)
results.Infof("updated labels")
return true
}

// This example uses a SetLabels object, which implements `Runner.Run` methods.
Expand All @@ -54,12 +56,12 @@ func (r *SetLabels) Run(ctx *fn.Context, functionConfig *fn.KubeObject, items fn
// kind: SetLabels
// metadata:
// name: setlabel-fn-config
func Example_asMain() {
func main() {
file, _ := os.Open("./data/setlabels-resourcelist.yaml")
defer file.Close()
os.Stdin = file

if err := fn.AsMain(&SetLabels{}); err != nil {
ctx := context.TODO()
if err := fn.AsMain(fn.WithContext(ctx, &SetLabels{})); err != nil {
os.Exit(1)
}
// Output:
Expand Down
11 changes: 10 additions & 1 deletion go/fn/functionrunner.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,14 @@
package fn

type Runner interface {
Run(context *Context, functionConfig *KubeObject, items KubeObjects)
// Run provides the entrypoint to allow you make changes to input `resourcelist.Items`
// Args:
// items: The KRM resources in the form of a slice of KubeObject.
// Note: You can only modify the existing items but not add or delete items.
// We intentionally design the method this way to make the Runner be used as a Transformer or Validator, but not a Generator.
// results: You can use `ErrorE` `Errorf` `Infof` `Warningf` `WarningE` to add user message to `Results`.
// Returns:
// return a boolean to tell whether the execution should be considered as PASS or FAIL. CLI like kpt will
// display the corresponding message.
Run(context *Context, functionConfig *KubeObject, items KubeObjects, results *Results) bool
}
118 changes: 59 additions & 59 deletions go/fn/result.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,54 +20,6 @@ import (
"strings"
)

// Context provides a series of functions to add `Result` to `ResourceList.Results`.
type Context struct {
results *Results
}

func (c *Context) Result(message string, severity Severity) {
*c.results = append(*c.results, &Result{Message: message, Severity: severity})
}

// AddErrResultAndDie adds an Error `Result` and terminates the KRM function run. `KubeObject` can be nil.
func (c *Context) ResultErrAndDie(message string, o *KubeObject) {
c.ResultErr(message, o)
panic(errResultEnd{obj: o, message: message})
}

// AddErrResult adds an Error `Result` and continues the KRM function run. `KubeObject` can be nil.
func (c *Context) ResultErr(message string, o *KubeObject) {
var r *Result
if o != nil {
r = ConfigObjectResult(message, o, Error)
} else {
r = GeneralResult(message, Error)
}
*c.results = append(*c.results, r)
}

// AddErrResult adds an Info `Result`. `KubeObject` can be nil.
func (c *Context) ResultInfo(message string, o *KubeObject) {
var r *Result
if o != nil {
r = ConfigObjectResult(message, o, Info)
} else {
r = GeneralResult(message, Info)
}
*c.results = append(*c.results, r)
}

// AddErrResult adds an Info `Result`. `KubeObject` can be nil.
func (c *Context) ResultWarn(message string, o *KubeObject) {
var r *Result
if o != nil {
r = ConfigObjectResult(message, o, Warning)
} else {
r = GeneralResult(message, Info)
}
*c.results = append(*c.results, r)
}

// Severity indicates the severity of the Result
type Severity string

Expand Down Expand Up @@ -178,18 +130,66 @@ type Field struct {

type Results []*Result

// Errorf writes an Error level `result` to the results slice. It accepts arguments according to a format specifier.
// e.g.
// results.Errorf("bad kind %v", "invalid")
func (r *Results) Errorf(format string, a ...any) {
errResult := &Result{Severity: Error, Message: fmt.Sprintf(format, a...)}
*r = append(*r, errResult)
}

// ErrorE writes the `error` as an Error level `result` to the results slice.
// e.g.
// err := error.New("test)
// results.ErrorE(err)
func (r *Results) ErrorE(err error) {
errResult := &Result{Severity: Error, Message: err.Error()}
*r = append(*r, errResult)
}

// Infof writes an Info level `result` to the results slice. It accepts arguments according to a format specifier.
// e.g.
// results.Infof("update %v %q ", "ConfigMap", "kptfile.kpt.dev")
func (r *Results) Infof(format string, a ...any) {
infoResult := &Result{Severity: Info, Message: fmt.Sprintf(format, a...)}
*r = append(*r, infoResult)
}

// Warningf writes a Warning level `result` to the results slice. It accepts arguments according to a format specifier.
// e.g.
// results.Warningf("bad kind %q", "invalid")
func (r *Results) Warningf(format string, a ...any) {
warnResult := &Result{Severity: Warning, Message: fmt.Sprintf(format, a...)}
*r = append(*r, warnResult)
}

// WarningE writes an error as a Warning level `result` to the results slice.
// Normally this function can be used for cases that need error tolerance.
func (r *Results) WarningE(err error) {
warnResult := &Result{Severity: Warning, Message: err.Error()}
*r = append(*r, warnResult)
}

func (r *Results) String() string {
var results []string
for _, result := range *r {
results = append(results, result.String())
}
return strings.Join(results, "\n---\n")
}

// Error enables Results to be returned as an error
func (e Results) Error() string {
func (r Results) Error() string {
var msgs []string
for _, i := range e {
for _, i := range r {
msgs = append(msgs, i.String())
}
return strings.Join(msgs, "\n\n")
}

// ExitCode provides the exit code based on the result's severity
func (e Results) ExitCode() int {
for _, i := range e {
func (r Results) ExitCode() int {
for _, i := range r {
if i.Severity == Error {
return 1
}
Expand All @@ -198,15 +198,15 @@ func (e Results) ExitCode() int {
}

// Sort performs an in place stable sort of Results
func (e Results) Sort() {
sort.SliceStable(e, func(i, j int) bool {
if fileLess(e, i, j) != 0 {
return fileLess(e, i, j) < 0
func (r Results) Sort() {
sort.SliceStable(r, func(i, j int) bool {
if fileLess(r, i, j) != 0 {
return fileLess(r, i, j) < 0
}
if severityLess(e, i, j) != 0 {
return severityLess(e, i, j) < 0
if severityLess(r, i, j) != 0 {
return severityLess(r, i, j) < 0
}
return resultToString(*e[i]) < resultToString(*e[j])
return resultToString(*r[i]) < resultToString(*r[j])
})
}

Expand Down
6 changes: 2 additions & 4 deletions go/fn/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,8 @@ func AsMain(input interface{}) error {
err := func() error {
var p ResourceListProcessor
switch input := input.(type) {
case Runner:
p = runnerProcessor{fnRunner: input}
case *Runner:
p = runnerProcessor{fnRunner: *input}
case runnerProcessor:
p = input
case ResourceListProcessorFunc:
p = input
default:
Expand Down
46 changes: 34 additions & 12 deletions go/fn/runnerProcessor.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,38 +15,60 @@
package fn

import (
"fmt"
"context"
"reflect"

"k8s.io/apimachinery/pkg/runtime/schema"
)

func WithContext(ctx context.Context, runner Runner) ResourceListProcessor {
return runnerProcessor{ctx: ctx, fnRunner: runner}
}

type runnerProcessor struct {
ctx context.Context
fnRunner Runner
}

func (r runnerProcessor) Process(rl *ResourceList) (bool, error) {
ctx := &Context{results: &rl.Results}
r.config(ctx, rl.FunctionConfig)
r.fnRunner.Run(ctx, rl.FunctionConfig, rl.Items)
return true, nil
fnCtx := &Context{Context: r.ctx}
results := new(Results)
if ok := r.config(rl.FunctionConfig, results); !ok {
rl.Results = append(rl.Results, *results...)
return false, nil
}
pass := r.fnRunner.Run(fnCtx, rl.FunctionConfig, rl.Items, results)
rl.Results = append(rl.Results, *results...)
return pass, nil
}

func (r *runnerProcessor) config(ctx *Context, o *KubeObject) {
func (r *runnerProcessor) config(o *KubeObject, results *Results) bool {
fnName := reflect.ValueOf(r.fnRunner).Elem().Type().Name()
switch true {
case o.IsEmpty():
ctx.Result("`FunctionConfig` is not given", Info)
case o.IsGVK("", "v1", "ConfigMap"):
data := o.NestedStringMapOrDie("data")
results.Infof("`FunctionConfig` is not given")
case o.IsGroupKind(schema.GroupKind{Kind: "ConfigMap"}):
data, _, err := o.NestedStringMap("data")
if err != nil {
results.ErrorE(err)
return false
}
fnRunnerElem := reflect.ValueOf(r.fnRunner).Elem()
for i := 0; i < fnRunnerElem.NumField(); i++ {
if fnRunnerElem.Field(i).Kind() == reflect.Map {
fnRunnerElem.Field(i).Set(reflect.ValueOf(data))
break
}
}
case o.IsGVK("fn.kpt.dev", "v1alpha1", fnName):
o.AsOrDie(r.fnRunner)
case o.IsGroupVersionKind(schema.GroupVersionKind{Group: "fn.kpt.dev", Version: "v1alpha1", Kind: fnName}):
err := o.As(r.fnRunner)
if err != nil {
results.ErrorE(err)
return false
}
default:
ctx.ResultErrAndDie(fmt.Sprintf("unknown FunctionConfig `%v`, expect %v", o.GetKind(), fnName), o)
results.Errorf("unknown FunctionConfig `%v`, expect %v", o.GetKind(), fnName)
return false
}
return true
}