lib/notifiers/jsonpath.go (115 lines of code) (raw):

package notifiers import ( "bytes" "context" "encoding/json" "fmt" "io" "reflect" "strings" "sync" cbpb "cloud.google.com/go/cloudbuild/apiv1/v2/cloudbuildpb" "k8s.io/client-go/third_party/forked/golang/template" "k8s.io/client-go/util/jsonpath" ) // BindingResolver is an object that given a Build and a way to get secrets, returns all bound substitutions from the // notifier configuration. type BindingResolver interface { Resolve(context.Context, SecretGetter, *cbpb.Build) (map[string]string, error) } type inputAndJSONPath struct { j *jsonpath.JSONPath // The JSONPath that parsed that path. p string // The user-provided path. } type jpResolver struct { mtx sync.RWMutex jps map[string]*inputAndJSONPath // Map of _SOME_SUBST_NAME => its inputAndJSONPath. cfg *Config } func newResolver(cfg *Config) (BindingResolver, error) { jps := map[string]*inputAndJSONPath{} for name, path := range cfg.Spec.Notification.Params { p, err := makeJSONPath(path) if err != nil { return nil, fmt.Errorf("failed to derive substitution path from %q: %v", path, err) } j := jsonpath.New(name).AllowMissingKeys(false) if err := j.Parse(p); err != nil { return nil, fmt.Errorf("failed to parse JSONPath expression from %q: %v", path, err) } jps[name] = &inputAndJSONPath{ j: j, p: path, // Use the user-provided path so error messages are easier to understand. } } return &jpResolver{ jps: jps, cfg: cfg, }, nil } func (j *jpResolver) Resolve(ctx context.Context, sg SecretGetter, build *cbpb.Build) (map[string]string, error) { j.mtx.RLock() defer j.mtx.RUnlock() // Use a "JSON" payload here since a struct would have export-field issues // based on the lowercase names. pld := map[string]interface{}{ "build": build, } ret := map[string]string{} for name, jp := range j.jps { fullResults, err := jp.j.FindResults(pld) if err != nil { return nil, fmt.Errorf("failed to parse %q with path %q from payload: %v", name, jp.p, err) } if len(fullResults) == 0 { return nil, fmt.Errorf("failed to get JSONPath query results for %q with path %q", name, jp.p) } buf := new(bytes.Buffer) for _, r := range fullResults { if err := printResults(buf, r); err != nil { return nil, err } } ret[name] = buf.String() } return ret, nil } func makeJSONPath(path string) (string, error) { if !strings.HasPrefix(path, "$(") || !strings.HasSuffix(path, ")") { return "", fmt.Errorf("expected %q to start with `$(` and end with `)` for a valid JSONPath expression", path) } trimmed := strings.TrimSuffix(strings.TrimPrefix(path, "$("), ")") return fmt.Sprintf("{ .%s }", trimmed), nil } /** NOTE; THE FOLLOWING HAS BEEN SHAMELESSLY COPIED FROM https://github.com/tektoncd/triggers/blob/master/pkg/template/jsonpath.go **/ func printResults(wr io.Writer, results []reflect.Value) error { for i, r := range results { text, err := textValue(r) if err != nil { return err } if i != len(results)-1 { text = append(text, ' ') } if _, err := wr.Write(text); err != nil { return err } } return nil } func textValue(v reflect.Value) ([]byte, error) { t := reflect.TypeOf(v.Interface()) // special case for null values in JSON; evalToText() returns <nil> here if t == nil { return []byte("null"), nil } switch t.Kind() { // evalToText() returns <map> ....; return JSON string instead. case reflect.Map, reflect.Slice: return json.Marshal(v.Interface()) default: return evalToText(v) } } func evalToText(v reflect.Value) ([]byte, error) { iface, ok := template.PrintableValue(v) if !ok { // only happens if v is a Chan or a Func return nil, fmt.Errorf("can't print type %s", v.Type()) } var buffer bytes.Buffer fmt.Fprint(&buffer, iface) return buffer.Bytes(), nil }