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  }