github.com/argoproj/argo-cd/v3@v3.2.1/util/argo/resource_tracking.go (about) 1 package argo 2 3 import ( 4 "errors" 5 "fmt" 6 "regexp" 7 "strings" 8 9 kubeutil "github.com/argoproj/gitops-engine/pkg/utils/kube" 10 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 11 12 "github.com/argoproj/argo-cd/v3/common" 13 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" 14 "github.com/argoproj/argo-cd/v3/util/kube" 15 ) 16 17 var ( 18 ErrWrongResourceTrackingFormat = errors.New("wrong resource tracking format, should be <application-name>:<group>/<kind>:<namespace>/<name>") 19 LabelMaxLength = 63 20 OkEndPattern = regexp.MustCompile("[a-zA-Z0-9]$") 21 ) 22 23 // ResourceTracking defines methods which allow setup and retrieve tracking information to resource 24 type ResourceTracking interface { 25 GetAppName(un *unstructured.Unstructured, key string, trackingMethod v1alpha1.TrackingMethod, installationID string) string 26 GetAppInstance(un *unstructured.Unstructured, trackingMethod v1alpha1.TrackingMethod, installationID string) *AppInstanceValue 27 SetAppInstance(un *unstructured.Unstructured, key, val, namespace string, trackingMethod v1alpha1.TrackingMethod, instanceID string) error 28 BuildAppInstanceValue(value AppInstanceValue) string 29 ParseAppInstanceValue(value string) (*AppInstanceValue, error) 30 Normalize(config, live *unstructured.Unstructured, labelKey, trackingMethod string) error 31 RemoveAppInstance(un *unstructured.Unstructured, trackingMethod string) error 32 } 33 34 // AppInstanceValue store information about resource tracking info 35 type AppInstanceValue struct { 36 ApplicationName string 37 Group string 38 Kind string 39 Namespace string 40 Name string 41 } 42 43 type resourceTracking struct{} 44 45 func NewResourceTracking() ResourceTracking { 46 return &resourceTracking{} 47 } 48 49 func IsOldTrackingMethod(trackingMethod string) bool { 50 return trackingMethod == "" || trackingMethod == string(v1alpha1.TrackingMethodLabel) 51 } 52 53 func (rt *resourceTracking) getAppInstanceValue(un *unstructured.Unstructured, installationID string) *AppInstanceValue { 54 if installationID != "" && un.GetAnnotations() == nil || un.GetAnnotations()[common.AnnotationInstallationID] != installationID { 55 return nil 56 } 57 appInstanceAnnotation, err := kube.GetAppInstanceAnnotation(un, common.AnnotationKeyAppInstance) 58 if err != nil { 59 return nil 60 } 61 value, err := rt.ParseAppInstanceValue(appInstanceAnnotation) 62 if err != nil { 63 return nil 64 } 65 return value 66 } 67 68 // GetAppName retrieve application name base on tracking method 69 func (rt *resourceTracking) GetAppName(un *unstructured.Unstructured, key string, trackingMethod v1alpha1.TrackingMethod, instanceID string) string { 70 retrieveAppInstanceValue := func() string { 71 value := rt.getAppInstanceValue(un, instanceID) 72 if value != nil { 73 return value.ApplicationName 74 } 75 return "" 76 } 77 switch trackingMethod { 78 case v1alpha1.TrackingMethodLabel: 79 label, err := kube.GetAppInstanceLabel(un, key) 80 if err != nil { 81 return "" 82 } 83 return label 84 case v1alpha1.TrackingMethodAnnotationAndLabel: 85 return retrieveAppInstanceValue() 86 case v1alpha1.TrackingMethodAnnotation: 87 return retrieveAppInstanceValue() 88 default: 89 return retrieveAppInstanceValue() 90 } 91 } 92 93 // GetAppInstance returns the representation of the app instance annotation. 94 // If the tracking method does not support metadata, or the annotation could 95 // not be parsed, it returns nil. 96 func (rt *resourceTracking) GetAppInstance(un *unstructured.Unstructured, trackingMethod v1alpha1.TrackingMethod, instanceID string) *AppInstanceValue { 97 switch trackingMethod { 98 case v1alpha1.TrackingMethodAnnotation, v1alpha1.TrackingMethodAnnotationAndLabel: 99 return rt.getAppInstanceValue(un, instanceID) 100 default: 101 return nil 102 } 103 } 104 105 // UnstructuredToAppInstanceValue will build the AppInstanceValue based 106 // on the provided unstructured. The given namespace works as a default 107 // value if the resource's namespace is not defined. It should be the 108 // Application's target destination namespace. 109 func UnstructuredToAppInstanceValue(un *unstructured.Unstructured, appName, namespace string) AppInstanceValue { 110 ns := un.GetNamespace() 111 if ns == "" { 112 ns = namespace 113 } 114 gvk := un.GetObjectKind().GroupVersionKind() 115 return AppInstanceValue{ 116 ApplicationName: appName, 117 Group: gvk.Group, 118 Kind: gvk.Kind, 119 Namespace: ns, 120 Name: un.GetName(), 121 } 122 } 123 124 // SetAppInstance set label/annotation base on tracking method 125 func (rt *resourceTracking) SetAppInstance(un *unstructured.Unstructured, key, val, namespace string, trackingMethod v1alpha1.TrackingMethod, instanceID string) error { 126 setAppInstanceAnnotation := func() error { 127 appInstanceValue := UnstructuredToAppInstanceValue(un, val, namespace) 128 if instanceID != "" { 129 if err := kube.SetAppInstanceAnnotation(un, common.AnnotationInstallationID, instanceID); err != nil { 130 return err 131 } 132 } else { 133 if err := kube.RemoveAnnotation(un, common.AnnotationInstallationID); err != nil { 134 return err 135 } 136 } 137 return kube.SetAppInstanceAnnotation(un, common.AnnotationKeyAppInstance, rt.BuildAppInstanceValue(appInstanceValue)) 138 } 139 switch trackingMethod { 140 case v1alpha1.TrackingMethodLabel: 141 err := kube.SetAppInstanceLabel(un, key, val) 142 if err != nil { 143 return fmt.Errorf("failed to set app instance label: %w", err) 144 } 145 return nil 146 case v1alpha1.TrackingMethodAnnotation: 147 return setAppInstanceAnnotation() 148 case v1alpha1.TrackingMethodAnnotationAndLabel: 149 err := setAppInstanceAnnotation() 150 if err != nil { 151 return err 152 } 153 if len(val) > LabelMaxLength { 154 val = val[:LabelMaxLength] 155 // Prevent errors if the truncated name ends in a special character. 156 // See https://github.com/argoproj/argo-cd/issues/18237. 157 for !OkEndPattern.MatchString(val) { 158 if len(val) <= 1 { 159 return errors.New("failed to set app instance label: unable to truncate label to not end with a special character") 160 } 161 val = val[:len(val)-1] 162 } 163 } 164 err = kube.SetAppInstanceLabel(un, key, val) 165 if err != nil { 166 return fmt.Errorf("failed to set app instance label: %w", err) 167 } 168 return nil 169 default: 170 return setAppInstanceAnnotation() 171 } 172 } 173 174 func (rt *resourceTracking) RemoveAppInstance(un *unstructured.Unstructured, trackingMethod string) error { 175 switch v1alpha1.TrackingMethod(trackingMethod) { 176 case v1alpha1.TrackingMethodLabel: 177 if err := kube.RemoveLabel(un, common.LabelKeyAppInstance); err != nil { 178 return err 179 } 180 return nil 181 case v1alpha1.TrackingMethodAnnotation: 182 if err := kube.RemoveAnnotation(un, common.AnnotationKeyAppInstance); err != nil { 183 return err 184 } 185 if err := kube.RemoveAnnotation(un, common.AnnotationInstallationID); err != nil { 186 return err 187 } 188 return nil 189 case v1alpha1.TrackingMethodAnnotationAndLabel: 190 if err := kube.RemoveAnnotation(un, common.AnnotationKeyAppInstance); err != nil { 191 return err 192 } 193 if err := kube.RemoveAnnotation(un, common.AnnotationInstallationID); err != nil { 194 return err 195 } 196 if err := kube.RemoveLabel(un, common.LabelKeyAppInstance); err != nil { 197 return err 198 } 199 return nil 200 default: 201 // By default, only app instance annotations are set and not labels 202 // hence the default case should be only to remove annotations and not labels 203 if err := kube.RemoveAnnotation(un, common.AnnotationKeyAppInstance); err != nil { 204 return err 205 } 206 if err := kube.RemoveAnnotation(un, common.AnnotationInstallationID); err != nil { 207 return err 208 } 209 } 210 211 return nil 212 } 213 214 // BuildAppInstanceValue build resource tracking id in format <application-name>;<group>/<kind>/<namespace>/<name> 215 func (rt *resourceTracking) BuildAppInstanceValue(value AppInstanceValue) string { 216 return fmt.Sprintf("%s:%s/%s:%s/%s", value.ApplicationName, value.Group, value.Kind, value.Namespace, value.Name) 217 } 218 219 // ParseAppInstanceValue parse resource tracking id from format <application-name>:<group>/<kind>:<namespace>/<name> to struct 220 func (rt *resourceTracking) ParseAppInstanceValue(value string) (*AppInstanceValue, error) { 221 var appInstanceValue AppInstanceValue 222 parts := strings.SplitN(value, ":", 3) 223 appInstanceValue.ApplicationName = parts[0] 224 if len(parts) != 3 { 225 return nil, ErrWrongResourceTrackingFormat 226 } 227 groupParts := strings.Split(parts[1], "/") 228 if len(groupParts) != 2 { 229 return nil, ErrWrongResourceTrackingFormat 230 } 231 nsParts := strings.Split(parts[2], "/") 232 if len(nsParts) != 2 { 233 return nil, ErrWrongResourceTrackingFormat 234 } 235 appInstanceValue.Group = groupParts[0] 236 appInstanceValue.Kind = groupParts[1] 237 appInstanceValue.Namespace = nsParts[0] 238 appInstanceValue.Name = nsParts[1] 239 return &appInstanceValue, nil 240 } 241 242 // Normalize updates live resource and removes diff caused by missing annotation or extra tracking label. 243 // The normalization is required to ensure smooth transition to new tracking method. 244 func (rt *resourceTracking) Normalize(config, live *unstructured.Unstructured, labelKey, trackingMethod string) error { 245 if IsOldTrackingMethod(trackingMethod) { 246 return nil 247 } 248 249 if live == nil || config == nil { 250 return nil 251 } 252 253 label, err := kube.GetAppInstanceLabel(live, labelKey) 254 if err != nil { 255 return fmt.Errorf("failed to get app instance label: %w", err) 256 } 257 if label == "" { 258 return nil 259 } 260 261 if kubeutil.IsCRD(live) { 262 // CRDs don't get tracking annotations. 263 return nil 264 } 265 266 annotation, err := kube.GetAppInstanceAnnotation(config, common.AnnotationKeyAppInstance) 267 if err != nil { 268 return err 269 } 270 err = kube.SetAppInstanceAnnotation(live, common.AnnotationKeyAppInstance, annotation) 271 if err != nil { 272 return err 273 } 274 275 label, err = kube.GetAppInstanceLabel(config, labelKey) 276 if err != nil { 277 return fmt.Errorf("failed to get app instance label: %w", err) 278 } 279 if label == "" { 280 err = kube.RemoveLabel(live, labelKey) 281 if err != nil { 282 return fmt.Errorf("failed to remove app instance label: %w", err) 283 } 284 } 285 286 return nil 287 }