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  }