github.com/argoproj/argo-events@v1.9.1/sensors/triggers/argo-workflow/argo-workflow.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 argo_workflow
    17  
    18  import (
    19  	"context"
    20  	"fmt"
    21  	"os"
    22  	"os/exec"
    23  	"strconv"
    24  	"strings"
    25  	"time"
    26  
    27  	"go.uber.org/zap"
    28  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    29  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    30  	"k8s.io/apimachinery/pkg/labels"
    31  	"k8s.io/apimachinery/pkg/runtime/schema"
    32  	"k8s.io/apimachinery/pkg/util/wait"
    33  	"k8s.io/client-go/dynamic"
    34  	"k8s.io/client-go/kubernetes"
    35  
    36  	"github.com/argoproj/argo-events/common/logging"
    37  	apicommon "github.com/argoproj/argo-events/pkg/apis/common"
    38  	"github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1"
    39  	"github.com/argoproj/argo-events/sensors/policy"
    40  	"github.com/argoproj/argo-events/sensors/triggers"
    41  )
    42  
    43  // ArgoWorkflowTrigger implements Trigger interface for Argo workflow
    44  type ArgoWorkflowTrigger struct {
    45  	// K8sClient is Kubernetes client
    46  	K8sClient kubernetes.Interface
    47  	// ArgoClient is Argo Workflow client
    48  	DynamicClient dynamic.Interface
    49  	// Sensor object
    50  	Sensor *v1alpha1.Sensor
    51  	// Trigger definition
    52  	Trigger *v1alpha1.Trigger
    53  	// logger to log stuff
    54  	Logger *zap.SugaredLogger
    55  
    56  	namespableDynamicClient dynamic.NamespaceableResourceInterface
    57  	cmdRunner               func(cmd *exec.Cmd) error
    58  }
    59  
    60  // NewArgoWorkflowTrigger returns a new Argo workflow trigger
    61  func NewArgoWorkflowTrigger(k8sClient kubernetes.Interface, dynamicClient dynamic.Interface, sensor *v1alpha1.Sensor, trigger *v1alpha1.Trigger, logger *zap.SugaredLogger) *ArgoWorkflowTrigger {
    62  	return &ArgoWorkflowTrigger{
    63  		K8sClient:     k8sClient,
    64  		DynamicClient: dynamicClient,
    65  		Sensor:        sensor,
    66  		Trigger:       trigger,
    67  		Logger:        logger.With(logging.LabelTriggerType, apicommon.ArgoWorkflowTrigger),
    68  		cmdRunner: func(cmd *exec.Cmd) error {
    69  			return cmd.Run()
    70  		},
    71  	}
    72  }
    73  
    74  // GetTriggerType returns the type of the trigger
    75  func (t *ArgoWorkflowTrigger) GetTriggerType() apicommon.TriggerType {
    76  	return apicommon.ArgoWorkflowTrigger
    77  }
    78  
    79  // FetchResource fetches the trigger resource from external source
    80  func (t *ArgoWorkflowTrigger) FetchResource(ctx context.Context) (interface{}, error) {
    81  	trigger := t.Trigger
    82  	return triggers.FetchKubernetesResource(trigger.Template.ArgoWorkflow.Source)
    83  }
    84  
    85  // ApplyResourceParameters applies parameters to the trigger resource
    86  func (t *ArgoWorkflowTrigger) ApplyResourceParameters(events map[string]*v1alpha1.Event, resource interface{}) (interface{}, error) {
    87  	obj, ok := resource.(*unstructured.Unstructured)
    88  	if !ok {
    89  		return nil, fmt.Errorf("failed to interpret the trigger resource")
    90  	}
    91  	if err := triggers.ApplyResourceParameters(events, t.Trigger.Template.ArgoWorkflow.Parameters, obj); err != nil {
    92  		return nil, err
    93  	}
    94  	return obj, nil
    95  }
    96  
    97  // Execute executes the trigger
    98  func (t *ArgoWorkflowTrigger) Execute(ctx context.Context, events map[string]*v1alpha1.Event, resource interface{}) (interface{}, error) {
    99  	trigger := t.Trigger
   100  
   101  	op := v1alpha1.Submit
   102  	if trigger.Template.ArgoWorkflow.Operation != "" {
   103  		op = trigger.Template.ArgoWorkflow.Operation
   104  	}
   105  
   106  	obj, ok := resource.(*unstructured.Unstructured)
   107  	if !ok {
   108  		return nil, fmt.Errorf("failed to interpret the trigger resource")
   109  	}
   110  
   111  	name := obj.GetName()
   112  	if name == "" {
   113  		if op != v1alpha1.Submit {
   114  			return nil, fmt.Errorf("failed to execute the workflow %v operation, no name is given", op)
   115  		}
   116  		if obj.GetGenerateName() == "" {
   117  			return nil, fmt.Errorf("failed to trigger the workflow, neither name nor generateName is given")
   118  		}
   119  	}
   120  
   121  	submittedWFLabels := make(map[string]string)
   122  	if op == v1alpha1.Submit {
   123  		submittedWFLabels["events.argoproj.io/sensor"] = t.Sensor.Name
   124  		submittedWFLabels["events.argoproj.io/trigger"] = trigger.Template.Name
   125  		submittedWFLabels["events.argoproj.io/action-timestamp"] = strconv.Itoa(int(time.Now().UnixNano() / int64(time.Millisecond)))
   126  	}
   127  
   128  	namespace := obj.GetNamespace()
   129  	if namespace == "" {
   130  		namespace = t.Sensor.Namespace
   131  	}
   132  
   133  	var cmd *exec.Cmd
   134  
   135  	switch op {
   136  	case v1alpha1.Submit:
   137  		file, err := os.CreateTemp("", fmt.Sprintf("%s%s", name, obj.GetGenerateName()))
   138  		if err != nil {
   139  			return nil, fmt.Errorf("failed to create a temp file for the workflow %s, %w", obj.GetName(), err)
   140  		}
   141  		defer os.Remove(file.Name())
   142  
   143  		// Add labels
   144  		labels := obj.GetLabels()
   145  		if labels == nil {
   146  			labels = make(map[string]string)
   147  		}
   148  		for k, v := range submittedWFLabels {
   149  			labels[k] = v
   150  		}
   151  		obj.SetLabels(labels)
   152  
   153  		jObj, err := obj.MarshalJSON()
   154  		if err != nil {
   155  			return nil, err
   156  		}
   157  
   158  		if _, err := file.Write(jObj); err != nil {
   159  			return nil, fmt.Errorf("failed to write workflow json %s to the temp file %s, %w", name, file.Name(), err)
   160  		}
   161  		cmd = exec.Command("argo", "-n", namespace, "submit", file.Name())
   162  	case v1alpha1.SubmitFrom:
   163  		kind := obj.GetKind()
   164  		switch strings.ToLower(kind) {
   165  		case "cronworkflow":
   166  			kind = "cronwf"
   167  		case "workflowtemplate":
   168  			kind = "workflowtemplate"
   169  		default:
   170  			return nil, fmt.Errorf("invalid kind %s", kind)
   171  		}
   172  		fromArg := fmt.Sprintf("%s/%s", kind, name)
   173  		cmd = exec.Command("argo", "-n", namespace, "submit", "--from", fromArg)
   174  	case v1alpha1.Resubmit:
   175  		cmd = exec.Command("argo", "-n", namespace, "resubmit", name)
   176  	case v1alpha1.Resume:
   177  		cmd = exec.Command("argo", "-n", namespace, "resume", name)
   178  	case v1alpha1.Retry:
   179  		cmd = exec.Command("argo", "-n", namespace, "retry", name)
   180  	case v1alpha1.Suspend:
   181  		cmd = exec.Command("argo", "-n", namespace, "suspend", name)
   182  	case v1alpha1.Terminate:
   183  		cmd = exec.Command("argo", "-n", namespace, "terminate", name)
   184  	case v1alpha1.Stop:
   185  		cmd = exec.Command("argo", "-n", namespace, "stop", name)
   186  	default:
   187  		return nil, fmt.Errorf("unknown operation type %s", string(op))
   188  	}
   189  
   190  	cmd.Stdout = os.Stdout
   191  	cmd.Stderr = os.Stderr
   192  	cmd.Args = append(cmd.Args, trigger.Template.ArgoWorkflow.Args...)
   193  	if err := t.cmdRunner(cmd); err != nil {
   194  		return nil, fmt.Errorf("failed to execute %s command for workflow %s, %w", string(op), name, err)
   195  	}
   196  
   197  	t.namespableDynamicClient = t.DynamicClient.Resource(schema.GroupVersionResource{
   198  		Group:    "argoproj.io",
   199  		Version:  "v1alpha1",
   200  		Resource: "workflows",
   201  	})
   202  
   203  	if op != v1alpha1.Submit {
   204  		return t.namespableDynamicClient.Namespace(namespace).Get(ctx, name, metav1.GetOptions{})
   205  	}
   206  	l, err := t.namespableDynamicClient.Namespace(namespace).List(ctx, metav1.ListOptions{LabelSelector: labels.SelectorFromSet(submittedWFLabels).String()})
   207  	if err != nil {
   208  		return nil, err
   209  	}
   210  	if len(l.Items) == 0 {
   211  		return nil, fmt.Errorf("failed to list created workflows for unknown reason")
   212  	}
   213  	return l.Items[0], nil
   214  }
   215  
   216  // ApplyPolicy applies the policy on the trigger
   217  func (t *ArgoWorkflowTrigger) ApplyPolicy(ctx context.Context, resource interface{}) error {
   218  	trigger := t.Trigger
   219  
   220  	if trigger.Policy == nil || trigger.Policy.K8s == nil || trigger.Policy.K8s.Labels == nil {
   221  		return nil
   222  	}
   223  
   224  	obj, ok := resource.(*unstructured.Unstructured)
   225  	if !ok {
   226  		return fmt.Errorf("failed to interpret the trigger resource")
   227  	}
   228  
   229  	p := policy.NewResourceLabels(trigger, t.namespableDynamicClient, obj)
   230  	if p == nil {
   231  		return nil
   232  	}
   233  
   234  	err := p.ApplyPolicy(ctx)
   235  	if err != nil {
   236  		switch err {
   237  		case wait.ErrWaitTimeout:
   238  			if trigger.Policy.K8s.ErrorOnBackoffTimeout {
   239  				return fmt.Errorf("failed to determine status of the triggered resource. setting trigger state as failed")
   240  			}
   241  			return nil
   242  		default:
   243  			return err
   244  		}
   245  	}
   246  
   247  	return nil
   248  }