github.com/argoproj/argo-events@v1.9.1/sensors/triggers/custom-trigger/custom-trigger.go (about) 1 /* 2 Copyright 2020 BlackRock, Inc. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 package customtrigger 17 18 import ( 19 "context" 20 "encoding/json" 21 "fmt" 22 23 "github.com/ghodss/yaml" 24 "go.uber.org/zap" 25 "google.golang.org/grpc" 26 "google.golang.org/grpc/connectivity" 27 "google.golang.org/grpc/credentials" 28 "google.golang.org/grpc/credentials/insecure" 29 "k8s.io/apimachinery/pkg/util/wait" 30 31 "github.com/argoproj/argo-events/common" 32 "github.com/argoproj/argo-events/common/logging" 33 apicommon "github.com/argoproj/argo-events/pkg/apis/common" 34 "github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1" 35 "github.com/argoproj/argo-events/sensors/triggers" 36 ) 37 38 // CustomTrigger implements Trigger interface for custom trigger resource 39 type CustomTrigger struct { 40 // Sensor object 41 Sensor *v1alpha1.Sensor 42 // Trigger definition 43 Trigger *v1alpha1.Trigger 44 // logger to log stuff 45 Logger *zap.SugaredLogger 46 // triggerClient is the gRPC client for the custom trigger server 47 triggerClient triggers.TriggerClient 48 } 49 50 // NewCustomTrigger returns a new custom trigger 51 func NewCustomTrigger(sensor *v1alpha1.Sensor, trigger *v1alpha1.Trigger, logger *zap.SugaredLogger, customTriggerClients common.StringKeyedMap[*grpc.ClientConn]) (*CustomTrigger, error) { 52 customTrigger := &CustomTrigger{ 53 Sensor: sensor, 54 Trigger: trigger, 55 Logger: logger.With(logging.LabelTriggerType, apicommon.CustomTrigger), 56 } 57 58 ct := trigger.Template.CustomTrigger 59 60 if conn, ok := customTriggerClients.Load(trigger.Template.Name); ok { 61 if conn.GetState() == connectivity.Ready { 62 logger.Info("trigger client connection is ready...") 63 customTrigger.triggerClient = triggers.NewTriggerClient(conn) 64 return customTrigger, nil 65 } 66 67 logger.Info("trigger client connection is closed, creating new one...") 68 customTriggerClients.Delete(trigger.Template.Name) 69 } 70 71 logger.Infow("instantiating trigger client...", zap.Any("server-url", ct.ServerURL)) 72 73 opt := []grpc.DialOption{ 74 grpc.WithBlock(), 75 grpc.WithTransportCredentials(insecure.NewCredentials()), 76 } 77 78 if ct.Secure { 79 var certFilePath string 80 var err error 81 switch { 82 case ct.CertSecret != nil: 83 certFilePath, err = common.GetSecretVolumePath(ct.CertSecret) 84 if err != nil { 85 return nil, err 86 } 87 default: 88 return nil, fmt.Errorf("invalid config, CERT secret not defined") 89 } 90 creds, err := credentials.NewClientTLSFromFile(certFilePath, ct.ServerNameOverride) 91 if err != nil { 92 return nil, err 93 } 94 opt = append(opt, grpc.WithTransportCredentials(creds)) 95 } 96 97 conn, err := grpc.Dial( 98 ct.ServerURL, 99 opt..., 100 ) 101 if err != nil { 102 return nil, err 103 } 104 105 backoff, err := common.Convert2WaitBackoff(&common.DefaultBackoff) 106 if err != nil { 107 return nil, err 108 } 109 110 if err = wait.ExponentialBackoff(*backoff, func() (done bool, err error) { 111 if conn.GetState() == connectivity.Ready { 112 return true, nil 113 } 114 return false, nil 115 }); err != nil { 116 return nil, err 117 } 118 119 customTrigger.triggerClient = triggers.NewTriggerClient(conn) 120 customTriggerClients.Store(trigger.Template.Name, conn) 121 122 logger.Info("successfully setup the trigger client...") 123 return customTrigger, nil 124 } 125 126 // GetTriggerType returns the type of the trigger 127 func (ct *CustomTrigger) GetTriggerType() apicommon.TriggerType { 128 return apicommon.CustomTrigger 129 } 130 131 // FetchResource fetches the trigger resource from external source 132 func (ct *CustomTrigger) FetchResource(ctx context.Context) (interface{}, error) { 133 specBody, err := yaml.Marshal(ct.Trigger.Template.CustomTrigger.Spec) 134 if err != nil { 135 return nil, fmt.Errorf("failed to parse the custom trigger spec body, %w", err) 136 } 137 138 ct.Logger.Debugw("trigger spec body", zap.Any("spec", string(specBody))) 139 140 resource, err := ct.triggerClient.FetchResource(context.Background(), &triggers.FetchResourceRequest{ 141 Resource: specBody, 142 }) 143 if err != nil { 144 return nil, fmt.Errorf("failed to fetch the custom trigger resource for %s, %w", ct.Trigger.Template.Name, err) 145 } 146 147 ct.Logger.Debugw("fetched resource", zap.Any("resource", string(resource.Resource))) 148 return resource.Resource, nil 149 } 150 151 // ApplyResourceParameters applies parameters to the trigger resource 152 func (ct *CustomTrigger) ApplyResourceParameters(events map[string]*v1alpha1.Event, resource interface{}) (interface{}, error) { 153 obj, ok := resource.([]byte) 154 if !ok { 155 return nil, fmt.Errorf("failed to interpret the trigger resource for resource parameters application") 156 } 157 parameters := ct.Trigger.Template.CustomTrigger.Parameters 158 159 if len(parameters) > 0 { 160 // only JSON formatted resource body is eligible for parameters 161 var temp map[string]interface{} 162 if err := json.Unmarshal(obj, &temp); err != nil { 163 return nil, fmt.Errorf("fetched resource body is not valid JSON for trigger %s, %w", ct.Trigger.Template.Name, err) 164 } 165 166 result, err := triggers.ApplyParams(obj, ct.Trigger.Template.CustomTrigger.Parameters, events) 167 if err != nil { 168 return nil, fmt.Errorf("failed to apply the parameters to the custom trigger resource for %s, %w", ct.Trigger.Template.Name, err) 169 } 170 171 ct.Logger.Debugw("resource after parameterization", zap.Any("resource", string(result))) 172 return result, nil 173 } 174 175 return resource, nil 176 } 177 178 // Execute executes the trigger 179 func (ct *CustomTrigger) Execute(ctx context.Context, events map[string]*v1alpha1.Event, resource interface{}) (interface{}, error) { 180 obj, ok := resource.([]byte) 181 if !ok { 182 return nil, fmt.Errorf("failed to interpret the trigger resource for the execution") 183 } 184 185 ct.Logger.Debugw("resource to execute", zap.Any("resource", string(obj))) 186 187 trigger := ct.Trigger.Template.CustomTrigger 188 189 var payload []byte 190 var err error 191 192 if trigger.Payload != nil { 193 payload, err = triggers.ConstructPayload(events, trigger.Payload) 194 if err != nil { 195 return nil, err 196 } 197 198 ct.Logger.Debugw("payload for the trigger execution", zap.Any("payload", string(payload))) 199 } 200 201 result, err := ct.triggerClient.Execute(context.Background(), &triggers.ExecuteRequest{ 202 Resource: obj, 203 Payload: payload, 204 }) 205 if err != nil { 206 return nil, fmt.Errorf("failed to execute the custom trigger resource for %s, %w", ct.Trigger.Template.Name, err) 207 } 208 209 ct.Logger.Debugw("trigger execution response", zap.Any("response", string(result.Response))) 210 return result.Response, nil 211 } 212 213 // ApplyPolicy applies the policy on the trigger 214 func (ct *CustomTrigger) ApplyPolicy(ctx context.Context, resource interface{}) error { 215 obj, ok := resource.([]byte) 216 if !ok { 217 return fmt.Errorf("failed to interpret the trigger resource for the policy application") 218 } 219 220 ct.Logger.Debugw("resource to apply policy on", zap.Any("resource", string(obj))) 221 222 result, err := ct.triggerClient.ApplyPolicy(ctx, &triggers.ApplyPolicyRequest{ 223 Request: obj, 224 }) 225 if err != nil { 226 return fmt.Errorf("failed to apply the policy for the custom trigger resource for %s, %w", ct.Trigger.Template.Name, err) 227 } 228 ct.Logger.Infow("policy application result", zap.Any("success", result.Success), zap.Any("message", result.Message)) 229 return err 230 }