github.com/argoproj/argo-events@v1.9.1/sensors/triggers/standard-k8s/standard-k8s.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 17 package standard_k8s 18 19 import ( 20 "context" 21 "fmt" 22 "strconv" 23 "time" 24 25 "github.com/imdario/mergo" 26 "go.uber.org/zap" 27 apierrors "k8s.io/apimachinery/pkg/api/errors" 28 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 30 "k8s.io/apimachinery/pkg/runtime" 31 k8stypes "k8s.io/apimachinery/pkg/types" 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 var clusterResources = map[string]bool{ 44 "namespaces": true, 45 "nodes": true, 46 } 47 48 // StandardK8STrigger implements Trigger interface for standard Kubernetes resources 49 type StandardK8sTrigger struct { 50 // K8sClient is kubernetes client 51 K8sClient kubernetes.Interface 52 // Dynamic client is Kubernetes dymalic client 53 DynamicClient dynamic.Interface 54 // Sensor object 55 Sensor *v1alpha1.Sensor 56 // Trigger definition 57 Trigger *v1alpha1.Trigger 58 // logger to log stuff 59 Logger *zap.SugaredLogger 60 61 namespableDynamicClient dynamic.NamespaceableResourceInterface 62 } 63 64 // NewStandardK8sTrigger returns a new StandardK8STrigger 65 func NewStandardK8sTrigger(k8sClient kubernetes.Interface, dynamicClient dynamic.Interface, sensor *v1alpha1.Sensor, trigger *v1alpha1.Trigger, logger *zap.SugaredLogger) *StandardK8sTrigger { 66 return &StandardK8sTrigger{ 67 K8sClient: k8sClient, 68 DynamicClient: dynamicClient, 69 Sensor: sensor, 70 Trigger: trigger, 71 Logger: logger.With(logging.LabelTriggerType, apicommon.K8sTrigger), 72 } 73 } 74 75 // GetTriggerType returns the type of the trigger 76 func (k8sTrigger *StandardK8sTrigger) GetTriggerType() apicommon.TriggerType { 77 return apicommon.K8sTrigger 78 } 79 80 // FetchResource fetches the trigger resource from external source 81 func (k8sTrigger *StandardK8sTrigger) FetchResource(ctx context.Context) (interface{}, error) { 82 trigger := k8sTrigger.Trigger 83 var rObj runtime.Object 84 85 uObj, err := triggers.FetchKubernetesResource(trigger.Template.K8s.Source) 86 if err != nil { 87 return nil, err 88 } 89 90 gvr := triggers.GetGroupVersionResource(uObj) 91 k8sTrigger.namespableDynamicClient = k8sTrigger.DynamicClient.Resource(gvr) 92 93 if trigger.Template.K8s.LiveObject && trigger.Template.K8s.Operation == v1alpha1.Update { 94 objName := uObj.GetName() 95 if objName == "" { 96 return nil, fmt.Errorf("resource name must be specified for fetching live object") 97 } 98 99 objNamespace := uObj.GetNamespace() 100 _, isClusterResource := clusterResources[gvr.Resource] 101 if objNamespace == "" && !isClusterResource { 102 return nil, fmt.Errorf("resource namespace must be specified for fetching live object") 103 } 104 rObj, err = k8sTrigger.namespableDynamicClient.Namespace(objNamespace).Get(ctx, objName, metav1.GetOptions{}) 105 if err != nil { 106 return nil, err 107 } 108 } else { 109 rObj = uObj 110 } 111 return rObj, nil 112 } 113 114 // ApplyResourceParameters applies parameters to the trigger resource 115 func (k8sTrigger *StandardK8sTrigger) ApplyResourceParameters(events map[string]*v1alpha1.Event, resource interface{}) (interface{}, error) { 116 obj, ok := resource.(*unstructured.Unstructured) 117 if !ok { 118 return nil, fmt.Errorf("failed to interpret the trigger resource") 119 } 120 if err := triggers.ApplyResourceParameters(events, k8sTrigger.Trigger.Template.K8s.Parameters, obj); err != nil { 121 return nil, err 122 } 123 return obj, nil 124 } 125 126 // Execute executes the trigger 127 func (k8sTrigger *StandardK8sTrigger) Execute(ctx context.Context, events map[string]*v1alpha1.Event, resource interface{}) (interface{}, error) { 128 trigger := k8sTrigger.Trigger 129 130 obj, ok := resource.(*unstructured.Unstructured) 131 if !ok { 132 return nil, fmt.Errorf("failed to interpret the trigger resource") 133 } 134 135 gvr := triggers.GetGroupVersionResource(obj) 136 namespace := "" 137 if _, isClusterResource := clusterResources[gvr.Resource]; !isClusterResource { 138 namespace = obj.GetNamespace() 139 // Defaults to sensor's namespace 140 if namespace == "" { 141 namespace = k8sTrigger.Sensor.Namespace 142 } 143 } 144 obj.SetNamespace(namespace) 145 146 op := v1alpha1.Create 147 if trigger.Template.K8s.Operation != "" { 148 op = trigger.Template.K8s.Operation 149 } 150 151 // We might have a client from FetchResource() already, or we might not have one yet. 152 if k8sTrigger.namespableDynamicClient == nil { 153 k8sTrigger.namespableDynamicClient = k8sTrigger.DynamicClient.Resource(gvr) 154 } 155 156 switch op { 157 case v1alpha1.Create: 158 k8sTrigger.Logger.Info("creating the object...") 159 // Add labels 160 labels := obj.GetLabels() 161 if labels == nil { 162 labels = make(map[string]string) 163 } 164 labels["events.argoproj.io/sensor"] = k8sTrigger.Sensor.Name 165 labels["events.argoproj.io/trigger"] = trigger.Template.Name 166 labels["events.argoproj.io/action-timestamp"] = strconv.Itoa(int(time.Now().UnixNano() / int64(time.Millisecond))) 167 obj.SetLabels(labels) 168 return k8sTrigger.namespableDynamicClient.Namespace(namespace).Create(ctx, obj, metav1.CreateOptions{}) 169 170 case v1alpha1.Update: 171 k8sTrigger.Logger.Info("updating the object...") 172 173 oldObj, err := k8sTrigger.namespableDynamicClient.Namespace(namespace).Get(ctx, obj.GetName(), metav1.GetOptions{}) 174 if err != nil && apierrors.IsNotFound(err) { 175 k8sTrigger.Logger.Info("object not found, creating the object...") 176 return k8sTrigger.namespableDynamicClient.Namespace(namespace).Create(ctx, obj, metav1.CreateOptions{}) 177 } else if err != nil { 178 return nil, fmt.Errorf("failed to retrieve existing object. err: %w", err) 179 } 180 181 if err := mergo.Merge(oldObj, obj, mergo.WithOverride); err != nil { 182 return nil, fmt.Errorf("failed to update the object. err: %w", err) 183 } 184 185 return k8sTrigger.namespableDynamicClient.Namespace(namespace).Update(ctx, oldObj, metav1.UpdateOptions{}) 186 187 case v1alpha1.Patch: 188 k8sTrigger.Logger.Info("patching the object...") 189 190 _, err := k8sTrigger.namespableDynamicClient.Namespace(namespace).Get(ctx, obj.GetName(), metav1.GetOptions{}) 191 if err != nil && apierrors.IsNotFound(err) { 192 k8sTrigger.Logger.Info("object not found, creating the object...") 193 return k8sTrigger.namespableDynamicClient.Namespace(namespace).Create(ctx, obj, metav1.CreateOptions{}) 194 } else if err != nil { 195 return nil, fmt.Errorf("failed to retrieve existing object. err: %w", err) 196 } 197 198 if k8sTrigger.Trigger.Template.K8s.PatchStrategy == "" { 199 k8sTrigger.Trigger.Template.K8s.PatchStrategy = k8stypes.MergePatchType 200 } 201 202 body, err := obj.MarshalJSON() 203 if err != nil { 204 return nil, fmt.Errorf("failed to marshal object into JSON schema. err: %w", err) 205 } 206 207 return k8sTrigger.namespableDynamicClient.Namespace(namespace).Patch(ctx, obj.GetName(), k8sTrigger.Trigger.Template.K8s.PatchStrategy, body, metav1.PatchOptions{}) 208 209 case v1alpha1.Delete: 210 k8sTrigger.Logger.Info("deleting the object...") 211 _, err := k8sTrigger.namespableDynamicClient.Namespace(namespace).Get(ctx, obj.GetName(), metav1.GetOptions{}) 212 213 if err != nil && apierrors.IsNotFound(err) { 214 k8sTrigger.Logger.Info("object not found, nothing to delete...") 215 return nil, nil 216 } else if err != nil { 217 return nil, fmt.Errorf("failed to retrieve existing object. err: %w", err) 218 } 219 220 err = k8sTrigger.namespableDynamicClient.Namespace(namespace).Delete(ctx, obj.GetName(), metav1.DeleteOptions{}) 221 if err != nil { 222 return nil, fmt.Errorf("failed to delete object. err: %w", err) 223 } 224 return nil, nil 225 226 default: 227 return nil, fmt.Errorf("unknown operation type %s", string(op)) 228 } 229 } 230 231 // ApplyPolicy applies the policy on the trigger 232 func (k8sTrigger *StandardK8sTrigger) ApplyPolicy(ctx context.Context, resource interface{}) error { 233 trigger := k8sTrigger.Trigger 234 235 if trigger.Policy == nil || trigger.Policy.K8s == nil || trigger.Policy.K8s.Labels == nil { 236 return nil 237 } 238 239 obj, ok := resource.(*unstructured.Unstructured) 240 if !ok { 241 return fmt.Errorf("failed to interpret the trigger resource") 242 } 243 244 p := policy.NewResourceLabels(trigger, k8sTrigger.namespableDynamicClient, obj) 245 if p == nil { 246 return nil 247 } 248 249 err := p.ApplyPolicy(ctx) 250 if err != nil { 251 switch err { 252 case wait.ErrWaitTimeout: 253 if trigger.Policy.K8s.ErrorOnBackoffTimeout { 254 return fmt.Errorf("failed to determine status of the triggered resource. setting trigger state as failed") 255 } 256 return nil 257 default: 258 return err 259 } 260 } 261 262 return nil 263 }