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 }