diff --git a/api/event-source.html b/api/event-source.html index bcbccbb5ee..d7bf5586bf 100644 --- a/api/event-source.html +++ b/api/event-source.html @@ -776,7 +776,7 @@

EventSourceSpec type
-github.com/argoproj/argo-events/pkg/apis/common.EventSourceType +Argo Events common.EventSourceType @@ -2649,5 +2649,5 @@

TLSConfig

Generated with gen-crd-api-reference-docs -on git commit 9f73feb. +on git commit 2bedd9d.

diff --git a/api/event-source.md b/api/event-source.md index 78ddafa3e3..5741da30da 100644 --- a/api/event-source.md +++ b/api/event-source.md @@ -1491,8 +1491,7 @@ Generic event source -type
-github.com/argoproj/argo-events/pkg/apis/common.EventSourceType +type
Argo Events common.EventSourceType @@ -5236,6 +5235,6 @@ ClientKeyPath refers the file path that contains client key.

Generated with gen-crd-api-reference-docs on git -commit 9f73feb. +commit 2bedd9d.

diff --git a/api/gateway.html b/api/gateway.html index add48f3414..5fa1431421 100644 --- a/api/gateway.html +++ b/api/gateway.html @@ -136,7 +136,7 @@

Gateway type
-github.com/argoproj/argo-events/pkg/apis/common.EventSourceType +Argo Events common.EventSourceType @@ -292,7 +292,7 @@

GatewaySpec type
-github.com/argoproj/argo-events/pkg/apis/common.EventSourceType +Argo Events common.EventSourceType @@ -647,5 +647,5 @@

Subscribers

Generated with gen-crd-api-reference-docs -on git commit 9f73feb. +on git commit 2bedd9d.

diff --git a/api/gateway.md b/api/gateway.md index 77bf840d51..e8ce0ba13c 100644 --- a/api/gateway.md +++ b/api/gateway.md @@ -270,8 +270,7 @@ configurations for the gateway -type
-github.com/argoproj/argo-events/pkg/apis/common.EventSourceType +type
Argo Events common.EventSourceType @@ -578,8 +577,7 @@ configurations for the gateway -type
-github.com/argoproj/argo-events/pkg/apis/common.EventSourceType +type
Argo Events common.EventSourceType @@ -1277,6 +1275,6 @@ NATS refers to the subscribers over NATS protocol.

Generated with gen-crd-api-reference-docs on git -commit 9f73feb. +commit 2bedd9d.

diff --git a/api/sensor.html b/api/sensor.html index 5580932119..c34ef13c46 100644 --- a/api/sensor.html +++ b/api/sensor.html @@ -224,7 +224,7 @@

ArtifactLocation s3
-github.com/argoproj/argo-events/pkg/apis/common.S3Artifact +Argo Events common.S3Artifact @@ -550,13 +550,17 @@

CustomTrigger -triggerBody
+spec
-string +map[string]string -

TriggerBody is the custom trigger resource specification that custom trigger gRPC server knows how to interpret.

+

Spec is the custom trigger resource specification that custom trigger gRPC server knows how to interpret.

+
+
+ +
@@ -3371,5 +3375,5 @@

URLArtifact

Generated with gen-crd-api-reference-docs -on git commit 9f73feb. +on git commit 2bedd9d.

diff --git a/api/sensor.md b/api/sensor.md index d82977af9f..7dddc330a3 100644 --- a/api/sensor.md +++ b/api/sensor.md @@ -470,8 +470,7 @@ Description -s3
-github.com/argoproj/argo-events/pkg/apis/common.S3Artifact +s3
Argo Events common.S3Artifact @@ -1132,7 +1131,7 @@ trigger gRPC server. -triggerBody
string +spec
map\[string\]string @@ -1140,11 +1139,17 @@ trigger gRPC server.

-TriggerBody is the custom trigger resource specification that custom -trigger gRPC server knows how to interpret. +Spec is the custom trigger resource specification that custom trigger +gRPC server knows how to interpret.

+

+ + + +
+ @@ -6699,6 +6704,6 @@ VerifyCert decides whether the connection is secure or not

Generated with gen-crd-api-reference-docs on git -commit 9f73feb. +commit 2bedd9d.

diff --git a/controllers/sensor/validate.go b/controllers/sensor/validate.go index 04c8739819..54ebe139a4 100644 --- a/controllers/sensor/validate.go +++ b/controllers/sensor/validate.go @@ -392,7 +392,7 @@ func validateCustomTrigger(trigger *v1alpha1.CustomTrigger) error { if trigger.ServerURL == "" { return errors.New("custom trigger gRPC server url is not defined") } - if trigger.TriggerBody == "" { + if trigger.Spec == nil { return errors.New("trigger body can't be empty") } if trigger.Secure { diff --git a/docs/triggers/build-your-own-trigger.md b/docs/triggers/build-your-own-trigger.md new file mode 100644 index 0000000000..772d5737b6 --- /dev/null +++ b/docs/triggers/build-your-own-trigger.md @@ -0,0 +1,112 @@ +# Build Your Own Trigger + +Argo Events supports a variety of triggers out of box like Argo Workflow, K8s Objects, AWS Lambda, HTTP Requests etc., but you may want to write your own logic to trigger a pipeline or create an object in K8s cluster. An example would be to trigger +TektonCD or AirFlow pipelines on GitHub events. + +## Custom Trigger + +In order to plug your own implementation for a trigger with Argo Events Sensor, you need to +run a gRPC server that implements the interface that the sensor expects. + +### Interface + +The interface exposed via proto file, + + // Trigger offers services to build a custom trigger + service Trigger { + // FetchResource fetches the resource to be triggered. + rpc FetchResource(FetchResourceRequest) returns (FetchResourceResponse); + // Execute executes the requested trigger resource. + rpc Execute(ExecuteRequest) returns (ExecuteResponse); + // ApplyPolicy applies policies on the trigger execution result. + rpc ApplyPolicy(ApplyPolicyRequest) returns (ApplyPolicyResponse); + } + +The complete proto file is available [here](https://github.com/argoproj/argo-events/blob/master/sensors/triggers/trigger.proto). + +Let's walk through the contract, + +1. `FetchResource`: If the trigger server needs to fetch a resource from external sources like S3, Git or a URL, this is the + place to do so. e.g. if the trigger server aims to invoke a TektonCD pipeline and the `PipelineRun` resource lives on Git, then + trigger server can first fetch it from Git and return it back to sensor. + +2. `Execute`: In this method, the trigger server executes/invokes the trigger. e.g. TektonCD pipeline resource being + created in K8s cluster. + +3. `ApplyPolicy`: This is where your trigger implementation can check whether the triggered resource transitioned into the success state. + Depending upon the response from the trigger server, the sensor will either stop processing subsequent triggers, or it will continue to + process them. + + +### How to define the Custom Trigger in a sensor? + +Let's look at the following sensor, + + apiVersion: argoproj.io/v1alpha1 + kind: Sensor + metadata: + name: webhook-sensor + labels: + sensors.argoproj.io/sensor-controller-instanceid: argo-events + spec: + template: + spec: + containers: + - name: sensor + image: metalgearsolid/sensor:v0.15.0 + imagePullPolicy: Always + serviceAccountName: argo-events-sa + dependencies: + - name: test-dep + gatewayName: webhook-gateway + eventName: example + subscription: + http: + port: 9300 + triggers: + - template: + name: webhook-workflow-trigger + custom: + # the url of the trigger server. + serverURL: tekton-trigger.argo-events.svc:9000 + # spec is map of string->string and it is sent over to trigger server. + # the spec can be anything you want as per your use-case, just make sure the trigger server understands the spec map. + spec: + url: "https://raw.githubusercontent.com/VaibhavPage/tekton-cd-trigger/master/example.yaml" + # These parameters are applied on resource fetched and returned by the trigger server. + # e.g. consider a trigger server which invokes TektonCD pipeline runs, then + # the trigger server can return a TektonCD PipelineRun resource. + # The parameters are then applied on that PipelineRun resource. + parameters: + - src: + dependencyName: test-dep + dataKey: body.namespace + dest: metadata.namespace + # These parameters are applied on entire template body. + # So that you can parameterize anything under `custom` key such as `serverURL`, `spec` etc. + parameters: + - src: + dependencyName: test-dep + dataKey: body.url + dest: custom.spec.url + +The sensor definition should look familiar to you. The only difference is the `custom` key under `triggers -> template`. +The specification under `custom` key defines the custom trigger. + +The most important fields are, + +1. `serverURL`: This is the URL of the trigger gRPC server. + +1. `spec`: It is a map of string -> string. The spec can be anything you want as per your use-case. The sensor sends + the spec to trigger server, and it is upto the trigger gRPC server to interpret the spec. + +1. `parameters`: The parameters override the resource that is fetched by the trigger server. + Read more info on parameters [here](https://argoproj.github.io/argo-events/tutorials/02-parameterization/). + +1. `payload`: Payload to send to the trigger server. Read more on payload [here](https://argoproj.github.io/argo-events/triggers/http-trigger/#request-payload). + +The complete spec for the custom trigger is available [here](https://github.com/argoproj/argo-events/blob/master/api/sensor.md#customtrigger). + +## Custom Trigger in Action + +Refer to a sample [trigger server](https://github.com/VaibhavPage/tekton-cd-trigger) that invokes TektonCD pipeline on events. diff --git a/examples/sensors/custom-trigger.yaml b/examples/sensors/custom-trigger.yaml new file mode 100644 index 0000000000..135367eb11 --- /dev/null +++ b/examples/sensors/custom-trigger.yaml @@ -0,0 +1,47 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Sensor +metadata: + name: webhook-sensor + labels: + sensors.argoproj.io/sensor-controller-instanceid: argo-events +spec: + template: + spec: + containers: + - name: sensor + image: metalgearsolid/sensor:v0.15.0 + imagePullPolicy: Always + serviceAccountName: argo-events-sa + dependencies: + - name: test-dep + gatewayName: webhook-gateway + eventName: example + subscription: + http: + port: 9300 + triggers: + - template: + name: webhook-workflow-trigger + custom: + # the url of the trigger server. + serverURL: tekton-trigger.argo-events.svc:9000 + # spec is map of string->string and it is sent over to trigger server. + # the spec can be anything you want as per your use-case, just make sure the trigger server understands the spec map. + spec: + url: "https://raw.githubusercontent.com/VaibhavPage/tekton-cd-trigger/master/example.yaml" + # These parameters are applied on resource fetched and returned by the trigger server. + # e.g. consider a trigger server which invokes TektonCD pipeline runs, then + # the trigger server can return a TektonCD PipelineRun resource. + # The parameters are then applied on that PipelineRun resource. + parameters: + - src: + dependencyName: test-dep + dataKey: body.namespace + dest: metadata.namespace + # These parameters are applied on entire template body. + # So that you can parameterize anything under `custom` key such as `serverURL`, `spec` etc. + parameters: + - src: + dependencyName: test-dep + dataKey: body.url + dest: custom.spec.url diff --git a/pkg/apis/sensor/v1alpha1/types.go b/pkg/apis/sensor/v1alpha1/types.go index 99ae8b6b6a..32d869e09c 100644 --- a/pkg/apis/sensor/v1alpha1/types.go +++ b/pkg/apis/sensor/v1alpha1/types.go @@ -486,8 +486,8 @@ type CustomTrigger struct { CertFilePath string `json:"certFilePath,omitempty" protobuf:"bytes,3,opt,name=certFilePath"` // ServerNameOverride for the secure connection between sensor and custom trigger gRPC server. ServerNameOverride string `json:"serverNameOverride,omitempty" protobuf:"bytes,4,opt,name=serverNameOverride"` - // TriggerBody is the custom trigger resource specification that custom trigger gRPC server knows how to interpret. - TriggerBody string `json:"triggerBody" protobuf:"bytes,5,name=triggerBody"` + // Spec is the custom trigger resource specification that custom trigger gRPC server knows how to interpret. + Spec map[string]string `json:"spec" protobuf:"bytes,5,name=spec"` // Parameters is the list of parameters that is applied to resolved custom trigger trigger object. // +listType=triggerParameters Parameters []TriggerParameter `json:"parameters,omitempty" protobuf:"bytes,6,rep,name=parameters"` diff --git a/pkg/apis/sensor/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/sensor/v1alpha1/zz_generated.deepcopy.go index c7b360e20e..38ca1f6ac8 100644 --- a/pkg/apis/sensor/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/sensor/v1alpha1/zz_generated.deepcopy.go @@ -211,6 +211,13 @@ func (in *ConfigmapArtifact) DeepCopy() *ConfigmapArtifact { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CustomTrigger) DeepCopyInto(out *CustomTrigger) { *out = *in + if in.Spec != nil { + in, out := &in.Spec, &out.Spec + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } if in.Parameters != nil { in, out := &in.Parameters, &out.Parameters *out = make([]TriggerParameter, len(*in)) diff --git a/sensors/triggers/custom-trigger/custom-trigger.go b/sensors/triggers/custom-trigger/custom-trigger.go index d58ba00546..aac7f3b1ba 100644 --- a/sensors/triggers/custom-trigger/custom-trigger.go +++ b/sensors/triggers/custom-trigger/custom-trigger.go @@ -17,10 +17,12 @@ package customtrigger import ( "context" + "encoding/json" "github.com/argoproj/argo-events/common" "github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1" "github.com/argoproj/argo-events/sensors/triggers" + "github.com/ghodss/yaml" "github.com/pkg/errors" "github.com/sirupsen/logrus" "google.golang.org/grpc" @@ -36,7 +38,7 @@ type CustomTrigger struct { // Trigger definition Trigger *v1alpha1.Trigger // logger to log stuff - Logger *logrus.Logger + Logger *logrus.Entry // triggerClient is the gRPC client for the custom trigger server triggerClient triggers.TriggerClient } @@ -46,19 +48,24 @@ func NewCustomTrigger(sensor *v1alpha1.Sensor, trigger *v1alpha1.Trigger, logger customTrigger := &CustomTrigger{ Sensor: sensor, Trigger: trigger, - Logger: logger, + Logger: logger.WithField("trigger-name", trigger.Template.Name), } ct := trigger.Template.CustomTrigger if conn, ok := customTriggerClients[trigger.Template.Name]; ok { if conn.GetState() == connectivity.Ready { + logger.Infoln("trigger client connection is ready...") customTrigger.triggerClient = triggers.NewTriggerClient(conn) return customTrigger, nil } + + logger.Infoln("trigger client connection is closed, creating new one...") delete(customTriggerClients, trigger.Template.Name) } + logger.WithField("server-url", ct.ServerURL).Infoln("instantiating trigger client...") + opt := []grpc.DialOption{ grpc.WithBlock(), grpc.WithInsecure(), @@ -93,18 +100,29 @@ func NewCustomTrigger(sensor *v1alpha1.Sensor, trigger *v1alpha1.Trigger, logger customTrigger.triggerClient = triggers.NewTriggerClient(conn) customTriggerClients[trigger.Template.Name] = conn + + logger.Infoln("successfully setup the trigger client...") return customTrigger, nil } // FetchResource fetches the trigger resource from external source func (ct *CustomTrigger) FetchResource() (interface{}, error) { + specBody, err := yaml.Marshal(ct.Trigger.Template.CustomTrigger.Spec) + if err != nil { + return nil, errors.Wrap(err, "failed to parse the custom trigger spec body") + } + + ct.Logger.WithField("spec", string(specBody)).Debugln("trigger spec body") + resource, err := ct.triggerClient.FetchResource(context.Background(), &triggers.FetchResourceRequest{ - Resource: []byte(ct.Trigger.Template.CustomTrigger.TriggerBody), + Resource: specBody, }) if err != nil { return nil, errors.Wrapf(err, "failed to fetch the custom trigger resource for %s", ct.Trigger.Template.Name) } - return resource, nil + + ct.Logger.WithField("resource", string(resource.Resource)).Debugln("fetched resource") + return resource.Resource, nil } // ApplyResourceParameters applies parameters to the trigger resource @@ -116,11 +134,19 @@ func (ct *CustomTrigger) ApplyResourceParameters(sensor *v1alpha1.Sensor, resour parameters := ct.Trigger.Template.CustomTrigger.Parameters if parameters != nil && len(parameters) > 0 { - resource, err := triggers.ApplyParams(obj, ct.Trigger.Template.CustomTrigger.Parameters, triggers.ExtractEvents(sensor, parameters)) + // only JSON formatted resource body is eligible for parameters + var temp map[string]interface{} + if err := json.Unmarshal(obj, &temp); err != nil { + return nil, errors.Wrapf(err, "fetched resource body is not valid JSON for trigger %s", ct.Trigger.Template.Name) + } + + result, err := triggers.ApplyParams(obj, ct.Trigger.Template.CustomTrigger.Parameters, triggers.ExtractEvents(sensor, parameters)) if err != nil { return nil, errors.Wrapf(err, "failed to apply the parameters to the custom trigger resource for %s", ct.Trigger.Template.Name) } - return resource, nil + + ct.Logger.WithField("resource", string(result)).Debugln("resource after parameterization") + return result, nil } return resource, nil @@ -132,14 +158,23 @@ func (ct *CustomTrigger) Execute(resource interface{}) (interface{}, error) { if !ok { return nil, errors.New("failed to interpret the trigger resource for the execution") } + + ct.Logger.WithField("resource", string(obj)).Debugln("resource to execute") + trigger := ct.Trigger.Template.CustomTrigger - if trigger.Payload == nil { - return nil, errors.New("payload parameters are not specified") - } - payload, err := triggers.ConstructPayload(ct.Sensor, trigger.Payload) - if err != nil { - return nil, err + + var payload []byte + var err error + + if trigger.Payload != nil { + payload, err = triggers.ConstructPayload(ct.Sensor, trigger.Payload) + if err != nil { + return nil, err + } + + ct.Logger.WithField("payload", string(payload)).Debugln("payload for the trigger execution") } + result, err := ct.triggerClient.Execute(context.Background(), &triggers.ExecuteRequest{ Resource: obj, Payload: payload, @@ -147,7 +182,9 @@ func (ct *CustomTrigger) Execute(resource interface{}) (interface{}, error) { if err != nil { return nil, errors.Wrapf(err, "failed to execute the custom trigger resource for %s", ct.Trigger.Template.Name) } - return result, nil + + ct.Logger.WithField("response", string(result.Response)).Debugln("trigger execution response") + return result.Response, nil } // ApplyPolicy applies the policy on the trigger @@ -156,6 +193,9 @@ func (ct *CustomTrigger) ApplyPolicy(resource interface{}) error { if !ok { return errors.New("failed to interpret the trigger resource for the policy application") } + + ct.Logger.WithField("resource", string(obj)).Debugln("resource to apply policy on") + result, err := ct.triggerClient.ApplyPolicy(context.Background(), &triggers.ApplyPolicyRequest{ Request: obj, })