github.com/argoproj/argo-cd/v3@v3.2.1/cmd/argocd/commands/admin/backup.go (about) 1 package admin 2 3 import ( 4 "bufio" 5 "fmt" 6 "io" 7 "os" 8 "strings" 9 "time" 10 11 "github.com/argoproj/gitops-engine/pkg/utils/kube" 12 log "github.com/sirupsen/logrus" 13 "github.com/spf13/cobra" 14 apierrors "k8s.io/apimachinery/pkg/api/errors" 15 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 16 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 17 "k8s.io/client-go/dynamic" 18 "k8s.io/client-go/tools/clientcmd" 19 20 "k8s.io/client-go/util/retry" 21 "sigs.k8s.io/yaml" 22 23 "github.com/argoproj/argo-cd/v3/cmd/argocd/commands/utils" 24 "github.com/argoproj/argo-cd/v3/common" 25 "github.com/argoproj/argo-cd/v3/pkg/apis/application" 26 "github.com/argoproj/argo-cd/v3/util/cli" 27 "github.com/argoproj/argo-cd/v3/util/errors" 28 "github.com/argoproj/argo-cd/v3/util/localconfig" 29 secutil "github.com/argoproj/argo-cd/v3/util/security" 30 ) 31 32 // NewExportCommand defines a new command for exporting Kubernetes and Argo CD resources. 33 func NewExportCommand() *cobra.Command { 34 var ( 35 clientConfig clientcmd.ClientConfig 36 out string 37 applicationNamespaces []string 38 applicationsetNamespaces []string 39 ) 40 command := cobra.Command{ 41 Use: "export", 42 Short: "Export all Argo CD data to stdout (default) or a file", 43 Run: func(c *cobra.Command, _ []string) { 44 ctx := c.Context() 45 46 config, err := clientConfig.ClientConfig() 47 errors.CheckError(err) 48 client, err := dynamic.NewForConfig(config) 49 errors.CheckError(err) 50 namespace, _, err := clientConfig.Namespace() 51 errors.CheckError(err) 52 acdClients := newArgoCDClientsets(config, namespace) 53 54 var writer io.Writer 55 if out == "-" { 56 writer = os.Stdout 57 } else { 58 f, err := os.Create(out) 59 errors.CheckError(err) 60 bw := bufio.NewWriter(f) 61 writer = bw 62 defer func() { 63 err = bw.Flush() 64 errors.CheckError(err) 65 err = f.Close() 66 errors.CheckError(err) 67 }() 68 } 69 70 if len(applicationNamespaces) == 0 || len(applicationsetNamespaces) == 0 { 71 defaultNs := getAdditionalNamespaces(ctx, acdClients.configMaps) 72 if len(applicationNamespaces) == 0 { 73 applicationNamespaces = defaultNs.applicationNamespaces 74 } 75 if len(applicationsetNamespaces) == 0 { 76 applicationsetNamespaces = defaultNs.applicationsetNamespaces 77 } 78 } 79 // To support applications and applicationsets in any namespace, we must list ALL namespaces and filter them afterwards 80 if len(applicationNamespaces) > 0 { 81 acdClients.applications = client.Resource(applicationsResource) 82 } 83 if len(applicationsetNamespaces) > 0 { 84 acdClients.applicationSets = client.Resource(appplicationSetResource) 85 } 86 87 acdConfigMap, err := acdClients.configMaps.Get(ctx, common.ArgoCDConfigMapName, metav1.GetOptions{}) 88 errors.CheckError(err) 89 export(writer, *acdConfigMap, namespace) 90 acdRBACConfigMap, err := acdClients.configMaps.Get(ctx, common.ArgoCDRBACConfigMapName, metav1.GetOptions{}) 91 errors.CheckError(err) 92 export(writer, *acdRBACConfigMap, namespace) 93 acdKnownHostsConfigMap, err := acdClients.configMaps.Get(ctx, common.ArgoCDKnownHostsConfigMapName, metav1.GetOptions{}) 94 errors.CheckError(err) 95 export(writer, *acdKnownHostsConfigMap, namespace) 96 acdTLSCertsConfigMap, err := acdClients.configMaps.Get(ctx, common.ArgoCDTLSCertsConfigMapName, metav1.GetOptions{}) 97 errors.CheckError(err) 98 export(writer, *acdTLSCertsConfigMap, namespace) 99 100 secrets, err := acdClients.secrets.List(ctx, metav1.ListOptions{}) 101 errors.CheckError(err) 102 for _, secret := range secrets.Items { 103 if isArgoCDSecret(secret) { 104 export(writer, secret, namespace) 105 } 106 } 107 108 projects, err := acdClients.projects.List(ctx, metav1.ListOptions{}) 109 errors.CheckError(err) 110 for _, proj := range projects.Items { 111 export(writer, proj, namespace) 112 } 113 114 applications, err := acdClients.applications.List(ctx, metav1.ListOptions{}) 115 errors.CheckError(err) 116 for _, app := range applications.Items { 117 // Export application only if it is in one of the enabled namespaces 118 if secutil.IsNamespaceEnabled(app.GetNamespace(), namespace, applicationNamespaces) { 119 export(writer, app, namespace) 120 } 121 } 122 applicationSets, err := acdClients.applicationSets.List(ctx, metav1.ListOptions{}) 123 if err != nil && !apierrors.IsNotFound(err) { 124 if apierrors.IsForbidden(err) { 125 log.Warn(err) 126 } else { 127 errors.CheckError(err) 128 } 129 } 130 if applicationSets != nil { 131 for _, appSet := range applicationSets.Items { 132 if secutil.IsNamespaceEnabled(appSet.GetNamespace(), namespace, applicationsetNamespaces) { 133 export(writer, appSet, namespace) 134 } 135 } 136 } 137 }, 138 } 139 140 clientConfig = cli.AddKubectlFlagsToCmd(&command) 141 command.Flags().StringVarP(&out, "out", "o", "-", "Output to the specified file instead of stdout") 142 command.Flags().StringSliceVarP(&applicationNamespaces, "application-namespaces", "", []string{}, fmt.Sprintf("Comma-separated list of namespace globs to export applications from, in addition to the control plane namespace (Argo CD namespace). "+ 143 "By default, all applications from the control plane namespace are always exported. "+ 144 "If this flag is provided, applications from the specified namespaces are exported along with the control plane namespace. "+ 145 "If not specified, the value from '%s' in %s is used (if defined in the ConfigMap). "+ 146 "If the ConfigMap value is not set, only applications from the control plane namespace are exported.", 147 applicationNamespacesCmdParamsKey, common.ArgoCDCmdParamsConfigMapName)) 148 command.Flags().StringSliceVarP(&applicationsetNamespaces, "applicationset-namespaces", "", []string{}, fmt.Sprintf("Comma-separated list of namespace globs to export ApplicationSets from, in addition to the control plane namespace (Argo CD namespace). "+ 149 "By default, all ApplicationSets from the control plane namespace are always exported. "+ 150 "If this flag is provided, ApplicationSets from the specified namespaces are exported along with the control plane namespace. "+ 151 "If not specified, the value from '%s' in %s is used (if defined in the ConfigMap). "+ 152 "If the ConfigMap value is not set, only ApplicationSets from the control plane namespace are exported.", 153 applicationsetNamespacesCmdParamsKey, common.ArgoCDCmdParamsConfigMapName)) 154 return &command 155 } 156 157 // NewImportCommand defines a new command for exporting Kubernetes and Argo CD resources. 158 func NewImportCommand() *cobra.Command { 159 var ( 160 clientConfig clientcmd.ClientConfig 161 prune bool 162 dryRun bool 163 verbose bool 164 stopOperation bool 165 ignoreTracking bool 166 overrideOnConflict bool 167 promptsEnabled bool 168 skipResourcesWithLabel string 169 applicationNamespaces []string 170 applicationsetNamespaces []string 171 ) 172 command := cobra.Command{ 173 Use: "import SOURCE", 174 Short: "Import Argo CD data from stdin (specify `-') or a file", 175 Run: func(c *cobra.Command, args []string) { 176 ctx := c.Context() 177 178 if len(args) != 1 { 179 c.HelpFunc()(c, args) 180 os.Exit(1) 181 } 182 config, err := clientConfig.ClientConfig() 183 errors.CheckError(err) 184 config.QPS = 100 185 config.Burst = 50 186 namespace, _, err := clientConfig.Namespace() 187 errors.CheckError(err) 188 acdClients := newArgoCDClientsets(config, namespace) 189 client, err := dynamic.NewForConfig(config) 190 errors.CheckError(err) 191 fmt.Printf("import process started %s\n", namespace) 192 tt := time.Now() 193 var input []byte 194 if in := args[0]; in == "-" { 195 input, err = io.ReadAll(os.Stdin) 196 } else { 197 input, err = os.ReadFile(in) 198 } 199 errors.CheckError(err) 200 var dryRunMsg string 201 if dryRun { 202 dryRunMsg = " (dry run)" 203 } 204 205 if len(applicationNamespaces) == 0 || len(applicationsetNamespaces) == 0 { 206 defaultNs := getAdditionalNamespaces(ctx, acdClients.configMaps) 207 if len(applicationNamespaces) == 0 { 208 applicationNamespaces = defaultNs.applicationNamespaces 209 } 210 if len(applicationsetNamespaces) == 0 { 211 applicationsetNamespaces = defaultNs.applicationsetNamespaces 212 } 213 } 214 // To support applications and applicationsets in any namespace, we must list ALL namespaces and filter them afterwards 215 if len(applicationNamespaces) > 0 { 216 acdClients.applications = client.Resource(applicationsResource) 217 } 218 if len(applicationsetNamespaces) > 0 { 219 acdClients.applicationSets = client.Resource(appplicationSetResource) 220 } 221 222 // pruneObjects tracks live objects, and it's current resource version. any remaining 223 // items in this map indicates the resource should be pruned since it no longer appears 224 // in the backup 225 pruneObjects := make(map[kube.ResourceKey]unstructured.Unstructured) 226 configMaps, err := acdClients.configMaps.List(ctx, metav1.ListOptions{}) 227 228 errors.CheckError(err) 229 for _, cm := range configMaps.Items { 230 if isArgoCDConfigMap(cm.GetName()) { 231 pruneObjects[kube.ResourceKey{Group: "", Kind: "ConfigMap", Name: cm.GetName(), Namespace: cm.GetNamespace()}] = cm 232 } 233 } 234 235 secrets, err := acdClients.secrets.List(ctx, metav1.ListOptions{}) 236 errors.CheckError(err) 237 for _, secret := range secrets.Items { 238 if isArgoCDSecret(secret) { 239 pruneObjects[kube.ResourceKey{Group: "", Kind: "Secret", Name: secret.GetName(), Namespace: secret.GetNamespace()}] = secret 240 } 241 } 242 applications, err := acdClients.applications.List(ctx, metav1.ListOptions{}) 243 errors.CheckError(err) 244 for _, app := range applications.Items { 245 if secutil.IsNamespaceEnabled(app.GetNamespace(), namespace, applicationNamespaces) { 246 pruneObjects[kube.ResourceKey{Group: application.Group, Kind: application.ApplicationKind, Name: app.GetName(), Namespace: app.GetNamespace()}] = app 247 } 248 } 249 projects, err := acdClients.projects.List(ctx, metav1.ListOptions{}) 250 errors.CheckError(err) 251 for _, proj := range projects.Items { 252 pruneObjects[kube.ResourceKey{Group: application.Group, Kind: application.AppProjectKind, Name: proj.GetName(), Namespace: proj.GetNamespace()}] = proj 253 } 254 applicationSets, err := acdClients.applicationSets.List(ctx, metav1.ListOptions{}) 255 if apierrors.IsForbidden(err) || apierrors.IsNotFound(err) { 256 log.Warnf("argoproj.io/ApplicationSet: %v\n", err) 257 } else { 258 errors.CheckError(err) 259 } 260 if applicationSets != nil { 261 for _, appSet := range applicationSets.Items { 262 if secutil.IsNamespaceEnabled(appSet.GetNamespace(), namespace, applicationsetNamespaces) { 263 pruneObjects[kube.ResourceKey{Group: application.Group, Kind: application.ApplicationSetKind, Name: appSet.GetName(), Namespace: appSet.GetNamespace()}] = appSet 264 } 265 } 266 } 267 // Create or replace existing object 268 backupObjects, err := kube.SplitYAML(input) 269 270 errors.CheckError(err) 271 for _, bakObj := range backupObjects { 272 gvk := bakObj.GroupVersionKind() 273 // For objects without namespace, assume they belong in ArgoCD namespace 274 if bakObj.GetNamespace() == "" { 275 bakObj.SetNamespace(namespace) 276 } 277 key := kube.ResourceKey{Group: gvk.Group, Kind: gvk.Kind, Name: bakObj.GetName(), Namespace: bakObj.GetNamespace()} 278 liveObj, exists := pruneObjects[key] 279 delete(pruneObjects, key) 280 281 // If the resource in backup matches the skip label, do not import it 282 if isSkipLabelMatches(bakObj, skipResourcesWithLabel) { 283 fmt.Printf("Skipping %s/%s %s in namespace %s\n", bakObj.GroupVersionKind().Group, bakObj.GroupVersionKind().Kind, bakObj.GetName(), bakObj.GetNamespace()) 284 continue 285 } 286 287 var dynClient dynamic.ResourceInterface 288 switch bakObj.GetKind() { 289 case "Secret": 290 dynClient = client.Resource(secretResource).Namespace(bakObj.GetNamespace()) 291 case "ConfigMap": 292 dynClient = client.Resource(configMapResource).Namespace(bakObj.GetNamespace()) 293 case application.AppProjectKind: 294 dynClient = client.Resource(appprojectsResource).Namespace(bakObj.GetNamespace()) 295 case application.ApplicationKind: 296 // If application is not in one of the allowed namespaces do not import it 297 if !secutil.IsNamespaceEnabled(bakObj.GetNamespace(), namespace, applicationNamespaces) { 298 continue 299 } 300 dynClient = client.Resource(applicationsResource).Namespace(bakObj.GetNamespace()) 301 case application.ApplicationSetKind: 302 // If applicationset is not in one of the allowed namespaces do not import it 303 if !secutil.IsNamespaceEnabled(bakObj.GetNamespace(), namespace, applicationsetNamespaces) { 304 continue 305 } 306 dynClient = client.Resource(appplicationSetResource).Namespace(bakObj.GetNamespace()) 307 } 308 309 // If there is a live object, remove the tracking annotations/label that might conflict 310 // when argo is managed with an application. 311 if ignoreTracking && exists { 312 updateTracking(bakObj, &liveObj) 313 } 314 315 switch { 316 case !exists: 317 isForbidden := false 318 if !dryRun { 319 _, err = dynClient.Create(ctx, bakObj, metav1.CreateOptions{}) 320 if apierrors.IsForbidden(err) || apierrors.IsNotFound(err) { 321 isForbidden = true 322 log.Warnf("%s/%s %s: %v", gvk.Group, gvk.Kind, bakObj.GetName(), err) 323 } else { 324 errors.CheckError(err) 325 } 326 } 327 if !isForbidden { 328 fmt.Printf("%s/%s %s in namespace %s created%s\n", gvk.Group, gvk.Kind, bakObj.GetName(), bakObj.GetNamespace(), dryRunMsg) 329 } 330 case specsEqual(*bakObj, liveObj) && checkAppHasNoNeedToStopOperation(liveObj, stopOperation): 331 if verbose { 332 fmt.Printf("%s/%s %s unchanged%s\n", gvk.Group, gvk.Kind, bakObj.GetName(), dryRunMsg) 333 } 334 default: 335 isForbidden := false 336 if !dryRun { 337 newLive := updateLive(bakObj, &liveObj, stopOperation) 338 _, err = dynClient.Update(ctx, newLive, metav1.UpdateOptions{}) 339 if apierrors.IsConflict(err) { 340 fmt.Printf("Failed to update %s/%s %s in namespace %s: %v\n", gvk.Group, gvk.Kind, bakObj.GetName(), bakObj.GetNamespace(), err) 341 if overrideOnConflict { 342 err = retry.RetryOnConflict(retry.DefaultRetry, func() error { 343 fmt.Printf("Resource conflict: retrying update for Group: %s, Kind: %s, Name: %s, Namespace: %s\n", gvk.Group, gvk.Kind, bakObj.GetName(), bakObj.GetNamespace()) 344 liveObj, getErr := dynClient.Get(ctx, newLive.GetName(), metav1.GetOptions{}) 345 if getErr != nil { 346 errors.CheckError(getErr) 347 } 348 newLive.SetResourceVersion(liveObj.GetResourceVersion()) 349 _, err = dynClient.Update(ctx, newLive, metav1.UpdateOptions{}) 350 return err 351 }) 352 } 353 } 354 if apierrors.IsForbidden(err) || apierrors.IsNotFound(err) { 355 isForbidden = true 356 log.Warnf("%s/%s %s: %v", gvk.Group, gvk.Kind, bakObj.GetName(), err) 357 } else { 358 errors.CheckError(err) 359 } 360 } 361 if !isForbidden { 362 fmt.Printf("%s/%s %s in namespace %s updated%s\n", gvk.Group, gvk.Kind, bakObj.GetName(), bakObj.GetNamespace(), dryRunMsg) 363 } 364 } 365 } 366 367 promptUtil := utils.NewPrompt(promptsEnabled) 368 369 // Delete objects not in backup 370 for key, liveObj := range pruneObjects { 371 // If a live resource has a label to skip the import, it should never be pruned 372 if isSkipLabelMatches(&liveObj, skipResourcesWithLabel) { 373 fmt.Printf("Skipping pruning of %s/%s %s in namespace %s\n", key.Group, key.Kind, liveObj.GetName(), liveObj.GetNamespace()) 374 continue 375 } 376 377 if prune { 378 var dynClient dynamic.ResourceInterface 379 switch key.Kind { 380 case "Secret": 381 dynClient = client.Resource(secretResource).Namespace(liveObj.GetNamespace()) 382 case application.AppProjectKind: 383 dynClient = client.Resource(appprojectsResource).Namespace(liveObj.GetNamespace()) 384 case application.ApplicationKind: 385 dynClient = client.Resource(applicationsResource).Namespace(liveObj.GetNamespace()) 386 if !dryRun { 387 if finalizers := liveObj.GetFinalizers(); len(finalizers) > 0 { 388 newLive := liveObj.DeepCopy() 389 newLive.SetFinalizers(nil) 390 _, err = dynClient.Update(ctx, newLive, metav1.UpdateOptions{}) 391 if err != nil && !apierrors.IsNotFound(err) { 392 errors.CheckError(err) 393 } 394 } 395 } 396 case application.ApplicationSetKind: 397 dynClient = client.Resource(appplicationSetResource).Namespace(liveObj.GetNamespace()) 398 default: 399 log.Fatalf("Unexpected kind '%s' in prune list", key.Kind) 400 } 401 isForbidden := false 402 403 if !dryRun { 404 canPrune := promptUtil.Confirm(fmt.Sprintf("Are you sure you want to prune %s/%s %s ? [y/n]", key.Group, key.Kind, key.Name)) 405 if canPrune { 406 err = dynClient.Delete(ctx, key.Name, metav1.DeleteOptions{}) 407 if apierrors.IsForbidden(err) || apierrors.IsNotFound(err) { 408 isForbidden = true 409 log.Warnf("%s/%s %s: %v\n", key.Group, key.Kind, key.Name, err) 410 } else { 411 errors.CheckError(err) 412 } 413 } else { 414 fmt.Printf("The command to prune %s/%s %s was cancelled.\n", key.Group, key.Kind, key.Name) 415 } 416 } 417 if !isForbidden { 418 fmt.Printf("%s/%s %s pruned%s\n", key.Group, key.Kind, key.Name, dryRunMsg) 419 } 420 } else { 421 fmt.Printf("%s/%s %s needs pruning\n", key.Group, key.Kind, key.Name) 422 } 423 } 424 duration := time.Since(tt) 425 fmt.Printf("Import process completed successfully in namespace %s at %s, duration: %s\n", namespace, time.Now().Format(time.RFC3339), duration) 426 }, 427 } 428 429 clientConfig = cli.AddKubectlFlagsToCmd(&command) 430 command.Flags().BoolVar(&dryRun, "dry-run", false, "Print what will be performed") 431 command.Flags().BoolVar(&prune, "prune", false, "Prune secrets, applications and projects which do not appear in the backup") 432 command.Flags().BoolVar(&ignoreTracking, "ignore-tracking", false, "Do not update the tracking annotation if the resource is already tracked") 433 command.Flags().BoolVar(&overrideOnConflict, "override-on-conflict", false, "Override the resource on conflict when updating resources") 434 command.Flags().BoolVar(&verbose, "verbose", false, "Verbose output (versus only changed output)") 435 command.Flags().BoolVar(&stopOperation, "stop-operation", false, "Stop any existing operations") 436 command.Flags().StringVarP(&skipResourcesWithLabel, "skip-resources-with-label", "", "", "Skip importing resources based on the label e.g. '--skip-resources-with-label my-label/example.io=true'") 437 command.Flags().StringSliceVarP(&applicationNamespaces, "application-namespaces", "", []string{}, fmt.Sprintf("Comma separated list of namespace globs to which import of applications is allowed. If not provided, value from '%s' in %s will be used. If it's not defined, only applications without an explicit namespace will be imported to the Argo CD namespace", applicationNamespacesCmdParamsKey, common.ArgoCDCmdParamsConfigMapName)) 438 command.Flags().StringSliceVarP(&applicationsetNamespaces, "applicationset-namespaces", "", []string{}, fmt.Sprintf("Comma separated list of namespace globs which import of applicationsets is allowed. If not provided, value from '%s' in %s will be used. If it's not defined, only applicationsets without an explicit namespace will be imported to the Argo CD namespace", applicationsetNamespacesCmdParamsKey, common.ArgoCDCmdParamsConfigMapName)) 439 command.PersistentFlags().BoolVar(&promptsEnabled, "prompts-enabled", localconfig.GetPromptsEnabled(true), "Force optional interactive prompts to be enabled or disabled, overriding local configuration. If not specified, the local configuration value will be used, which is false by default.") 440 return &command 441 } 442 443 // check app has no need to stop operation. 444 func checkAppHasNoNeedToStopOperation(liveObj unstructured.Unstructured, stopOperation bool) bool { 445 if !stopOperation { 446 return true 447 } 448 if liveObj.GetKind() == application.ApplicationKind { 449 return liveObj.Object["operation"] == nil 450 } 451 return true 452 } 453 454 // export writes the unstructured object and removes extraneous cruft from output before writing 455 func export(w io.Writer, un unstructured.Unstructured, argocdNamespace string) { 456 name := un.GetName() 457 finalizers := un.GetFinalizers() 458 apiVersion := un.GetAPIVersion() 459 kind := un.GetKind() 460 labels := un.GetLabels() 461 annotations := un.GetAnnotations() 462 namespace := un.GetNamespace() 463 unstructured.RemoveNestedField(un.Object, "metadata") 464 un.SetName(name) 465 un.SetFinalizers(finalizers) 466 un.SetAPIVersion(apiVersion) 467 un.SetKind(kind) 468 un.SetLabels(labels) 469 un.SetAnnotations(annotations) 470 if namespace != argocdNamespace { 471 // Explicitly add the namespace for appset and apps in any namespace 472 un.SetNamespace(namespace) 473 } 474 data, err := yaml.Marshal(un.Object) 475 errors.CheckError(err) 476 _, err = w.Write(data) 477 errors.CheckError(err) 478 _, err = w.Write([]byte(yamlSeparator)) 479 errors.CheckError(err) 480 } 481 482 // updateLive replaces the live object's finalizers, spec, annotations, labels, and data from the 483 // backup object but leaves all other fields intact (status, other metadata, etc...) 484 func updateLive(bak, live *unstructured.Unstructured, stopOperation bool) *unstructured.Unstructured { 485 newLive := live.DeepCopy() 486 newLive.SetAnnotations(bak.GetAnnotations()) 487 newLive.SetLabels(bak.GetLabels()) 488 newLive.SetFinalizers(bak.GetFinalizers()) 489 switch live.GetKind() { 490 case "Secret", "ConfigMap": 491 newLive.Object["data"] = bak.Object["data"] 492 case application.AppProjectKind: 493 newLive.Object["spec"] = bak.Object["spec"] 494 case application.ApplicationKind: 495 newLive.Object["spec"] = bak.Object["spec"] 496 if _, ok := bak.Object["status"]; ok { 497 newLive.Object["status"] = bak.Object["status"] 498 } 499 if stopOperation { 500 newLive.Object["operation"] = nil 501 } 502 503 case "ApplicationSet": 504 newLive.Object["spec"] = bak.Object["spec"] 505 } 506 return newLive 507 } 508 509 // updateTracking will update the tracking label and annotation in the bak resources to the 510 // value of the live resource. 511 func updateTracking(bak, live *unstructured.Unstructured) { 512 // update the common annotation 513 bakAnnotations := bak.GetAnnotations() 514 liveAnnotations := live.GetAnnotations() 515 if liveAnnotations != nil && bakAnnotations != nil { 516 if v, ok := liveAnnotations[common.AnnotationKeyAppInstance]; ok { 517 if _, ok := bakAnnotations[common.AnnotationKeyAppInstance]; ok { 518 bakAnnotations[common.AnnotationKeyAppInstance] = v 519 bak.SetAnnotations(bakAnnotations) 520 } 521 } 522 } 523 524 // update the common label 525 // A custom label can be set, but it is impossible to know which instance is managing the application 526 bakLabels := bak.GetLabels() 527 liveLabels := live.GetLabels() 528 if liveLabels != nil && bakLabels != nil { 529 if v, ok := liveLabels[common.LabelKeyAppInstance]; ok { 530 if _, ok := bakLabels[common.LabelKeyAppInstance]; ok { 531 bakLabels[common.LabelKeyAppInstance] = v 532 bak.SetLabels(bakLabels) 533 } 534 } 535 } 536 } 537 538 // isSkipLabelMatches return if the resource should be skipped based on the labels 539 func isSkipLabelMatches(obj *unstructured.Unstructured, skipResourcesWithLabel string) bool { 540 if skipResourcesWithLabel == "" { 541 return false 542 } 543 parts := strings.SplitN(skipResourcesWithLabel, "=", 2) 544 if len(parts) != 2 || parts[0] == "" || parts[1] == "" { 545 return false 546 } 547 key, value := parts[0], parts[1] 548 if val, ok := obj.GetLabels()[key]; ok && val == value { 549 return true 550 } 551 return false 552 }