diff --git a/api/v1/receiver_types.go b/api/v1/receiver_types.go
index 9790345d4..046fc0a4a 100644
--- a/api/v1/receiver_types.go
+++ b/api/v1/receiver_types.go
@@ -63,10 +63,22 @@ type ReceiverSpec struct {
// +optional
Events []string `json:"events,omitempty"`
+ // TODO: Validate one or other (or both?)
+
// A list of resources to be notified about changes.
// +required
Resources []CrossNamespaceObjectReference `json:"resources"`
+ // ResourceExpressions is a list of CEL expressions that will be parsed to
+ // determine resources to be notified about changes.
+ // The expressions must evaluate to CEL values that contain the keys "name",
+ // "kind", "apiVersion" and optionally "namespace".
+ // These values will be parsed to CrossNamespaceObjectReferences.
+ // e.g. {"name": "test-resource-1", "kind": "Receiver", "apiVersion":
+ // "notification.toolkit.fluxcd.io/v1"}.
+ // +optional
+ ResourceExpressions []string `json:"resourceExpressions,omitempty"`
+
// SecretRef specifies the Secret containing the token used
// to validate the payload authenticity.
// +required
diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go
index 81a647ce8..8e0bbd010 100644
--- a/api/v1/zz_generated.deepcopy.go
+++ b/api/v1/zz_generated.deepcopy.go
@@ -126,6 +126,11 @@ func (in *ReceiverSpec) DeepCopyInto(out *ReceiverSpec) {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
+ if in.ResourceExpressions != nil {
+ in, out := &in.ResourceExpressions, &out.ResourceExpressions
+ *out = make([]string, len(*in))
+ copy(*out, *in)
+ }
out.SecretRef = in.SecretRef
}
diff --git a/config/crd/bases/notification.toolkit.fluxcd.io_receivers.yaml b/config/crd/bases/notification.toolkit.fluxcd.io_receivers.yaml
index 239a1a81f..bb80b7177 100644
--- a/config/crd/bases/notification.toolkit.fluxcd.io_receivers.yaml
+++ b/config/crd/bases/notification.toolkit.fluxcd.io_receivers.yaml
@@ -62,6 +62,18 @@ spec:
Secret references.
pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$
type: string
+ resourceExpressions:
+ description: |-
+ ResourceExpressions is a list of CEL expressions that will be parsed to
+ determine resources to be notified about changes.
+ The expressions must evaluate to CEL values that contain the keys "name",
+ "kind", "apiVersion" and optionally "namespace".
+ These values will be parsed to CrossNamespaceObjectReferences.
+ e.g. {"name": "test-resource-1", "kind": "Receiver", "apiVersion":
+ "notification.toolkit.fluxcd.io/v1"}.
+ items:
+ type: string
+ type: array
resources:
description: A list of resources to be notified about changes.
items:
diff --git a/docs/api/v1/notification.md b/docs/api/v1/notification.md
index 325c17596..bf2143aba 100644
--- a/docs/api/v1/notification.md
+++ b/docs/api/v1/notification.md
@@ -122,6 +122,18 @@ e.g. ‘push’ for GitHub or ‘Push Hook’ for GitLab.
+resourceExpressions
+
+[]string
+
+ |
+
+ ResourceExpressions is a list of CEL expressions that will be parsed to
+determine resources to be notified about changes.
+ |
+
+
+
secretRef
@@ -321,6 +333,18 @@ e.g. ‘push’ for GitHub or ‘Push Hook’ for GitLab.
|
+resourceExpressions
+
+[]string
+
+ |
+
+ ResourceExpressions is a list of CEL expressions that will be parsed to
+determine resources to be notified about changes.
+ |
+
+
+
secretRef
diff --git a/go.mod b/go.mod
index 7329b41ad..276aaf095 100644
--- a/go.mod
+++ b/go.mod
@@ -25,6 +25,7 @@ require (
github.com/fluxcd/pkg/ssa v0.41.0
github.com/getsentry/sentry-go v0.28.1
github.com/go-logr/logr v1.4.2
+ github.com/google/cel-go v0.20.1
github.com/google/go-github/v63 v63.0.0
github.com/hashicorp/go-retryablehttp v0.7.7
github.com/ktrysmt/go-bitbucket v0.9.80
@@ -39,6 +40,7 @@ require (
golang.org/x/oauth2 v0.22.0
golang.org/x/text v0.17.0
google.golang.org/api v0.192.0
+ google.golang.org/protobuf v1.34.2
k8s.io/api v0.31.0
k8s.io/apimachinery v0.31.0
k8s.io/client-go v0.31.0
@@ -70,6 +72,7 @@ require (
github.com/DataDog/zstd v1.5.2 // indirect
github.com/MakeNowJust/heredoc v1.0.0 // indirect
github.com/ProtonMail/go-crypto v1.0.0 // indirect
+ github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
@@ -151,6 +154,7 @@ require (
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 // indirect
github.com/spf13/cobra v1.8.1 // indirect
+ github.com/stoewer/go-strcase v1.2.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xlab/treeprint v1.2.0 // indirect
go.opencensus.io v0.24.0 // indirect
@@ -175,7 +179,6 @@ require (
google.golang.org/genproto/googleapis/api v0.0.0-20240725223205-93522f1f2a9f // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf // indirect
google.golang.org/grpc v1.65.0 // indirect
- google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
diff --git a/go.sum b/go.sum
index eb9234fe2..b8ef31c7d 100644
--- a/go.sum
+++ b/go.sum
@@ -65,6 +65,8 @@ github.com/PagerDuty/go-pagerduty v1.8.0 h1:MTFqTffIcAervB83U7Bx6HERzLbyaSPL/+ox
github.com/PagerDuty/go-pagerduty v1.8.0/go.mod h1:nzIeAqyFSJAFkjWKvMzug0JtwDg+V+UoCWjFrfFH5mI=
github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
+github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
+github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -201,6 +203,8 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
+github.com/google/cel-go v0.20.1 h1:nDx9r8S3L4pE61eDdt8igGj8rf5kjYR3ILxWIpWNi84=
+github.com/google/cel-go v0.20.1/go.mod h1:kWcIzTsPX0zmQ+H3TirHstLLf9ep5QTsZBN9u4dOYLg=
github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@@ -365,12 +369,15 @@ github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU=
+github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -574,6 +581,7 @@ gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSP
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
diff --git a/internal/server/receiver_handler_test.go b/internal/server/receiver_handler_test.go
index e460f72f1..1971e960e 100644
--- a/internal/server/receiver_handler_test.go
+++ b/internal/server/receiver_handler_test.go
@@ -760,6 +760,140 @@ func Test_handlePayload(t *testing.T) {
expectedResourcesAnnotated: 1,
expectedResponseCode: http.StatusOK,
},
+ {
+ name: "resources determined by CEL expressions",
+ headers: map[string]string{
+ "Content-Type": "application/json; charset=utf-8",
+ },
+ receiver: &apiv1.Receiver{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "receiver",
+ },
+ Spec: apiv1.ReceiverSpec{
+ Type: apiv1.GenericReceiver,
+ SecretRef: meta.LocalObjectReference{
+ Name: "token",
+ },
+ ResourceExpressions: []string{
+ `{"name": "test-resource-1", "kind": "Receiver", "apiVersion": "notification.toolkit.fluxcd.io/v1"}`,
+ `[{"name": body.image.split(':',2)[0] + '-2', "namespace": "tested", "kind": "Receiver", "apiVersion": "notification.toolkit.fluxcd.io/v1"}]`,
+ `body.resources.map(r, {"name": r, "kind": "Receiver", "apiVersion": "notification.toolkit.fluxcd.io/v1"})`,
+ },
+ },
+ Status: apiv1.ReceiverStatus{
+ WebhookPath: apiv1.ReceiverWebhookPath,
+ Conditions: []metav1.Condition{{Type: meta.ReadyCondition, Status: metav1.ConditionTrue}},
+ },
+ },
+ secret: &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "token",
+ },
+ Data: map[string][]byte{
+ "token": []byte("token"),
+ },
+ },
+ payload: map[string]interface{}{
+ "image": "test-resource:1.2.1",
+ "resources": []string{
+ "test-resource-3",
+ "test-resource-4",
+ },
+ },
+ resources: []client.Object{
+ &apiv1.Receiver{
+ TypeMeta: metav1.TypeMeta{
+ Kind: apiv1.ReceiverKind,
+ APIVersion: apiv1.GroupVersion.String(),
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-resource-1",
+ },
+ },
+ &apiv1.Receiver{
+ TypeMeta: metav1.TypeMeta{
+ Kind: apiv1.ReceiverKind,
+ APIVersion: apiv1.GroupVersion.String(),
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-resource-2",
+ Namespace: "tested",
+ },
+ },
+ &apiv1.Receiver{
+ TypeMeta: metav1.TypeMeta{
+ Kind: apiv1.ReceiverKind,
+ APIVersion: apiv1.GroupVersion.String(),
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-resource-3",
+ },
+ },
+ &apiv1.Receiver{
+ TypeMeta: metav1.TypeMeta{
+ Kind: apiv1.ReceiverKind,
+ APIVersion: apiv1.GroupVersion.String(),
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-resource-4",
+ },
+ },
+ },
+ expectedResourcesAnnotated: 4, // TODO: This should really check more than just the count.
+ expectedResponseCode: http.StatusOK,
+ },
+ {
+ name: "handling errors when parsing the CEL expression results",
+ headers: map[string]string{
+ "Content-Type": "application/json; charset=utf-8",
+ },
+ receiver: &apiv1.Receiver{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "receiver",
+ },
+ Spec: apiv1.ReceiverSpec{
+ Type: apiv1.GenericReceiver,
+ SecretRef: meta.LocalObjectReference{
+ Name: "token",
+ },
+ ResourceExpressions: []string{
+ `{"name": ["test-resource-1"], "kind": "Receiver", "apiVersion": "notification.toolkit.fluxcd.io/v1"}`,
+ },
+ },
+ Status: apiv1.ReceiverStatus{
+ WebhookPath: apiv1.ReceiverWebhookPath,
+ Conditions: []metav1.Condition{{Type: meta.ReadyCondition, Status: metav1.ConditionTrue}},
+ },
+ },
+ secret: &corev1.Secret{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "token",
+ },
+ Data: map[string][]byte{
+ "token": []byte("token"),
+ },
+ },
+ payload: map[string]interface{}{
+ "image": "test-resource:1.2.1",
+ "resources": []string{
+ "test-resource-3",
+ "test-resource-4",
+ },
+ },
+ resources: []client.Object{
+ &apiv1.Receiver{
+ TypeMeta: metav1.TypeMeta{
+ Kind: apiv1.ReceiverKind,
+ APIVersion: apiv1.GroupVersion.String(),
+ },
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-resource-1",
+ },
+ },
+ },
+ expectedResourcesAnnotated: 0, // TODO: This should really check more than just the count.
+ expectedResponseCode: http.StatusBadRequest,
+ },
}
scheme := runtime.NewScheme()
diff --git a/internal/server/receiver_handlers.go b/internal/server/receiver_handlers.go
index 558c6082d..fd350fb1b 100644
--- a/internal/server/receiver_handlers.go
+++ b/internal/server/receiver_handlers.go
@@ -26,8 +26,10 @@ import (
"errors"
"fmt"
"io"
+ "mime"
"net/http"
"net/url"
+ "reflect"
"strings"
"time"
@@ -35,7 +37,12 @@ import (
"github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/runtime/conditions"
"github.com/go-logr/logr"
+ "github.com/google/cel-go/cel"
+ "github.com/google/cel-go/checker/decls"
+ "github.com/google/cel-go/common/types/traits"
+ celext "github.com/google/cel-go/ext"
"github.com/google/go-github/v63/github"
+ "google.golang.org/protobuf/types/known/structpb"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
@@ -122,6 +129,20 @@ func (s *ReceiverServer) handlePayload() func(w http.ResponseWriter, r *http.Req
}
}
+ evaluatedResources, err := s.evaluateResourceExpressions(r, receiver)
+ if err != nil {
+ logger.Error(err, "unable to evaluate resource expressions")
+ w.WriteHeader(http.StatusBadRequest)
+ return
+ }
+
+ for _, resource := range evaluatedResources {
+ if err := s.requestReconciliation(ctx, logger, resource, receiver.Namespace); err != nil {
+ logger.Error(err, "unable to request reconciliation")
+ withErrors = true
+ }
+ }
+
if withErrors {
w.WriteHeader(http.StatusInternalServerError)
} else {
@@ -544,3 +565,135 @@ func getGroupVersion(s string) (string, string) {
return slice[0], slice[1]
}
+
+func (s *ReceiverServer) evaluateResourceExpressions(r *http.Request, receiver apiv1.Receiver) ([]apiv1.CrossNamespaceObjectReference, error) {
+ if len(receiver.Spec.ResourceExpressions) == 0 {
+ return nil, nil
+ }
+
+ body := map[string]any{}
+ // Only decodes the body for the expression if the body is JSON.
+ // Technically you could generate several resources without any body.
+ if isJSONContent(r) {
+ if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
+ return nil, fmt.Errorf("failed to parse request body as JSON: %s", err)
+ }
+ }
+
+ env, err := makeCELEnv()
+ if err != nil {
+ return nil, fmt.Errorf("failed to setup CEL environment: %s", err)
+ }
+
+ var combinedResources []apiv1.CrossNamespaceObjectReference
+
+ var returnErr error
+ for _, expr := range receiver.Spec.ResourceExpressions {
+ evaluated, evalErr := evaluate(expr, env, map[string]interface{}{
+ "body": body,
+ })
+ if evalErr != nil {
+ returnErr = errors.Join(returnErr, evalErr)
+ continue
+ }
+ combinedResources = append(combinedResources, evaluated...)
+ }
+
+ return combinedResources, returnErr
+}
+
+func evaluate(expr string, env *cel.Env, data map[string]any) ([]apiv1.CrossNamespaceObjectReference, error) {
+ parsed, issues := env.Parse(expr)
+ if issues != nil && issues.Err() != nil {
+ return nil, fmt.Errorf("failed to parse expression %v: %w", expr, issues.Err())
+ }
+
+ checked, issues := env.Check(parsed)
+ if issues != nil && issues.Err() != nil {
+ return nil, fmt.Errorf("expression %v check failed: %w", expr, issues.Err())
+ }
+
+ prg, err := env.Program(checked, cel.EvalOptions(cel.OptOptimize))
+ if err != nil {
+ return nil, fmt.Errorf("expression %v failed to create a Program: %w", expr, err)
+ }
+
+ out, _, err := prg.Eval(data)
+ if err != nil {
+ return nil, fmt.Errorf("expression %v failed to evaluate: %w", expr, err)
+ }
+
+ switch v := out.(type) {
+ case traits.Lister:
+ return parseList(v)
+ case traits.Mapper:
+ ref, err := mapperToReference(v)
+ if err != nil {
+ return nil, err
+ }
+ return []apiv1.CrossNamespaceObjectReference{*ref}, nil
+ // TODO Log out other types?
+ }
+
+ return nil, nil
+}
+
+func parseList(l traits.Lister) ([]apiv1.CrossNamespaceObjectReference, error) {
+ var resources []apiv1.CrossNamespaceObjectReference
+ it := l.Iterator()
+ for it.HasNext().Value() == true {
+ switch element := it.Next().(type) {
+ case traits.Mapper:
+ cno, err := mapperToReference(element)
+ if err != nil {
+ return nil, err
+ }
+ resources = append(resources, *cno)
+ }
+ }
+
+ return resources, nil
+}
+
+func mapperToReference(v traits.Mapper) (*apiv1.CrossNamespaceObjectReference, error) {
+ raw, err := v.ConvertToNative(reflect.TypeOf(&structpb.Value{}))
+ if err != nil {
+ return nil, fmt.Errorf("failed to convert to resource reference: %s", err)
+ }
+ cno := apiv1.CrossNamespaceObjectReference{}
+ b, err := raw.(*structpb.Value).MarshalJSON()
+ if err != nil {
+ return nil, fmt.Errorf("converting object references to JSON: %w", err)
+ }
+ err = json.Unmarshal(b, &cno)
+ if err != nil {
+ return nil, fmt.Errorf("parsing object reference: %w", err)
+ }
+
+ return &cno, err
+}
+
+func makeCELEnv() (*cel.Env, error) {
+ mapStrDyn := decls.NewMapType(decls.String, decls.Dyn)
+ return cel.NewEnv(
+ celext.Strings(),
+ celext.Encoders(),
+ cel.Declarations(
+ decls.NewVar("body", mapStrDyn),
+ ))
+}
+
+func isJSONContent(r *http.Request) bool {
+ contentType := r.Header.Get("Content-type")
+ for _, v := range strings.Split(contentType, ",") {
+ t, _, err := mime.ParseMediaType(v)
+ if err != nil {
+ break
+ }
+ if t == "application/json" {
+ return true
+ }
+ }
+
+ return false
+}
|