github.com/argoproj/argo-cd@v1.8.7/cmd/argocd-util/commands/argocd_util.go (about)

     1  package commands
     2  
     3  import (
     4  	"bufio"
     5  	"context"
     6  	"fmt"
     7  	"io"
     8  	"io/ioutil"
     9  	"os"
    10  	"os/exec"
    11  	"reflect"
    12  	"syscall"
    13  
    14  	"github.com/argoproj/gitops-engine/pkg/utils/kube"
    15  	"github.com/ghodss/yaml"
    16  	log "github.com/sirupsen/logrus"
    17  	"github.com/spf13/cobra"
    18  	apiv1 "k8s.io/api/core/v1"
    19  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    20  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    21  	"k8s.io/apimachinery/pkg/runtime"
    22  	"k8s.io/apimachinery/pkg/runtime/schema"
    23  	"k8s.io/client-go/dynamic"
    24  	"k8s.io/client-go/kubernetes"
    25  	"k8s.io/client-go/rest"
    26  	"k8s.io/client-go/tools/clientcmd"
    27  
    28  	"github.com/argoproj/argo-cd/common"
    29  	"github.com/argoproj/argo-cd/util/cli"
    30  	"github.com/argoproj/argo-cd/util/db"
    31  	"github.com/argoproj/argo-cd/util/dex"
    32  	"github.com/argoproj/argo-cd/util/errors"
    33  	"github.com/argoproj/argo-cd/util/settings"
    34  )
    35  
    36  const (
    37  	// CLIName is the name of the CLI
    38  	cliName = "argocd-util"
    39  	// YamlSeparator separates sections of a YAML file
    40  	yamlSeparator = "---\n"
    41  )
    42  
    43  var (
    44  	configMapResource    = schema.GroupVersionResource{Group: "", Version: "v1", Resource: "configmaps"}
    45  	secretResource       = schema.GroupVersionResource{Group: "", Version: "v1", Resource: "secrets"}
    46  	applicationsResource = schema.GroupVersionResource{Group: "argoproj.io", Version: "v1alpha1", Resource: "applications"}
    47  	appprojectsResource  = schema.GroupVersionResource{Group: "argoproj.io", Version: "v1alpha1", Resource: "appprojects"}
    48  )
    49  
    50  // NewCommand returns a new instance of an argocd command
    51  func NewCommand() *cobra.Command {
    52  	var (
    53  		logFormat string
    54  		logLevel  string
    55  	)
    56  
    57  	var command = &cobra.Command{
    58  		Use:               cliName,
    59  		Short:             "argocd-util tools used by Argo CD",
    60  		Long:              "argocd-util has internal utility tools used by Argo CD",
    61  		DisableAutoGenTag: true,
    62  		Run: func(c *cobra.Command, args []string) {
    63  			c.HelpFunc()(c, args)
    64  		},
    65  	}
    66  
    67  	command.AddCommand(cli.NewVersionCmd(cliName))
    68  	command.AddCommand(NewRunDexCommand())
    69  	command.AddCommand(NewGenDexConfigCommand())
    70  	command.AddCommand(NewImportCommand())
    71  	command.AddCommand(NewExportCommand())
    72  	command.AddCommand(NewClusterConfig())
    73  	command.AddCommand(NewProjectsCommand())
    74  	command.AddCommand(NewSettingsCommand())
    75  	command.AddCommand(NewAppsCommand())
    76  
    77  	command.Flags().StringVar(&logFormat, "logformat", "text", "Set the logging format. One of: text|json")
    78  	command.Flags().StringVar(&logLevel, "loglevel", "info", "Set the logging level. One of: debug|info|warn|error")
    79  	return command
    80  }
    81  
    82  func NewRunDexCommand() *cobra.Command {
    83  	var (
    84  		clientConfig clientcmd.ClientConfig
    85  	)
    86  	var command = cobra.Command{
    87  		Use:   "rundex",
    88  		Short: "Runs dex generating a config using settings from the Argo CD configmap and secret",
    89  		RunE: func(c *cobra.Command, args []string) error {
    90  			_, err := exec.LookPath("dex")
    91  			errors.CheckError(err)
    92  			config, err := clientConfig.ClientConfig()
    93  			errors.CheckError(err)
    94  			namespace, _, err := clientConfig.Namespace()
    95  			errors.CheckError(err)
    96  			kubeClientset := kubernetes.NewForConfigOrDie(config)
    97  			settingsMgr := settings.NewSettingsManager(context.Background(), kubeClientset, namespace)
    98  			prevSettings, err := settingsMgr.GetSettings()
    99  			errors.CheckError(err)
   100  			updateCh := make(chan *settings.ArgoCDSettings, 1)
   101  			settingsMgr.Subscribe(updateCh)
   102  
   103  			for {
   104  				var cmd *exec.Cmd
   105  				dexCfgBytes, err := dex.GenerateDexConfigYAML(prevSettings)
   106  				errors.CheckError(err)
   107  				if len(dexCfgBytes) == 0 {
   108  					log.Infof("dex is not configured")
   109  				} else {
   110  					err = ioutil.WriteFile("/tmp/dex.yaml", dexCfgBytes, 0644)
   111  					errors.CheckError(err)
   112  					log.Debug(redactor(string(dexCfgBytes)))
   113  					cmd = exec.Command("dex", "serve", "/tmp/dex.yaml")
   114  					cmd.Stdout = os.Stdout
   115  					cmd.Stderr = os.Stderr
   116  					err = cmd.Start()
   117  					errors.CheckError(err)
   118  				}
   119  
   120  				// loop until the dex config changes
   121  				for {
   122  					newSettings := <-updateCh
   123  					newDexCfgBytes, err := dex.GenerateDexConfigYAML(newSettings)
   124  					errors.CheckError(err)
   125  					if string(newDexCfgBytes) != string(dexCfgBytes) {
   126  						prevSettings = newSettings
   127  						log.Infof("dex config modified. restarting dex")
   128  						if cmd != nil && cmd.Process != nil {
   129  							err = cmd.Process.Signal(syscall.SIGTERM)
   130  							errors.CheckError(err)
   131  							_, err = cmd.Process.Wait()
   132  							errors.CheckError(err)
   133  						}
   134  						break
   135  					} else {
   136  						log.Infof("dex config unmodified")
   137  					}
   138  				}
   139  			}
   140  		},
   141  	}
   142  
   143  	clientConfig = cli.AddKubectlFlagsToCmd(&command)
   144  	return &command
   145  }
   146  
   147  func NewGenDexConfigCommand() *cobra.Command {
   148  	var (
   149  		clientConfig clientcmd.ClientConfig
   150  		out          string
   151  	)
   152  	var command = cobra.Command{
   153  		Use:   "gendexcfg",
   154  		Short: "Generates a dex config from Argo CD settings",
   155  		RunE: func(c *cobra.Command, args []string) error {
   156  			config, err := clientConfig.ClientConfig()
   157  			errors.CheckError(err)
   158  			namespace, _, err := clientConfig.Namespace()
   159  			errors.CheckError(err)
   160  			kubeClientset := kubernetes.NewForConfigOrDie(config)
   161  			settingsMgr := settings.NewSettingsManager(context.Background(), kubeClientset, namespace)
   162  			settings, err := settingsMgr.GetSettings()
   163  			errors.CheckError(err)
   164  			dexCfgBytes, err := dex.GenerateDexConfigYAML(settings)
   165  			errors.CheckError(err)
   166  			if len(dexCfgBytes) == 0 {
   167  				log.Infof("dex is not configured")
   168  				return nil
   169  			}
   170  			if out == "" {
   171  				dexCfg := make(map[string]interface{})
   172  				err := yaml.Unmarshal(dexCfgBytes, &dexCfg)
   173  				errors.CheckError(err)
   174  				if staticClientsInterface, ok := dexCfg["staticClients"]; ok {
   175  					if staticClients, ok := staticClientsInterface.([]interface{}); ok {
   176  						for i := range staticClients {
   177  							staticClient := staticClients[i]
   178  							if mappings, ok := staticClient.(map[string]interface{}); ok {
   179  								for key := range mappings {
   180  									if key == "secret" {
   181  										mappings[key] = "******"
   182  									}
   183  								}
   184  								staticClients[i] = mappings
   185  							}
   186  						}
   187  						dexCfg["staticClients"] = staticClients
   188  					}
   189  				}
   190  				errors.CheckError(err)
   191  				maskedDexCfgBytes, err := yaml.Marshal(dexCfg)
   192  				errors.CheckError(err)
   193  				fmt.Print(string(maskedDexCfgBytes))
   194  			} else {
   195  				err = ioutil.WriteFile(out, dexCfgBytes, 0644)
   196  				errors.CheckError(err)
   197  			}
   198  			return nil
   199  		},
   200  	}
   201  
   202  	clientConfig = cli.AddKubectlFlagsToCmd(&command)
   203  	command.Flags().StringVarP(&out, "out", "o", "", "Output to the specified file instead of stdout")
   204  	return &command
   205  }
   206  
   207  // NewImportCommand defines a new command for exporting Kubernetes and Argo CD resources.
   208  func NewImportCommand() *cobra.Command {
   209  	var (
   210  		clientConfig clientcmd.ClientConfig
   211  		prune        bool
   212  		dryRun       bool
   213  	)
   214  	var command = cobra.Command{
   215  		Use:   "import SOURCE",
   216  		Short: "Import Argo CD data from stdin (specify `-') or a file",
   217  		Run: func(c *cobra.Command, args []string) {
   218  			if len(args) != 1 {
   219  				c.HelpFunc()(c, args)
   220  				os.Exit(1)
   221  			}
   222  			config, err := clientConfig.ClientConfig()
   223  			errors.CheckError(err)
   224  			config.QPS = 100
   225  			config.Burst = 50
   226  			errors.CheckError(err)
   227  			namespace, _, err := clientConfig.Namespace()
   228  			errors.CheckError(err)
   229  			acdClients := newArgoCDClientsets(config, namespace)
   230  
   231  			var input []byte
   232  			if in := args[0]; in == "-" {
   233  				input, err = ioutil.ReadAll(os.Stdin)
   234  			} else {
   235  				input, err = ioutil.ReadFile(in)
   236  			}
   237  			errors.CheckError(err)
   238  			var dryRunMsg string
   239  			if dryRun {
   240  				dryRunMsg = " (dry run)"
   241  			}
   242  
   243  			// pruneObjects tracks live objects and it's current resource version. any remaining
   244  			// items in this map indicates the resource should be pruned since it no longer appears
   245  			// in the backup
   246  			pruneObjects := make(map[kube.ResourceKey]unstructured.Unstructured)
   247  			configMaps, err := acdClients.configMaps.List(context.Background(), metav1.ListOptions{})
   248  			errors.CheckError(err)
   249  			// referencedSecrets holds any secrets referenced in the argocd-cm configmap. These
   250  			// secrets need to be imported too
   251  			var referencedSecrets map[string]bool
   252  			for _, cm := range configMaps.Items {
   253  				if isArgoCDConfigMap(cm.GetName()) {
   254  					pruneObjects[kube.ResourceKey{Group: "", Kind: "ConfigMap", Name: cm.GetName()}] = cm
   255  				}
   256  				if cm.GetName() == common.ArgoCDConfigMapName {
   257  					referencedSecrets = getReferencedSecrets(cm)
   258  				}
   259  			}
   260  
   261  			secrets, err := acdClients.secrets.List(context.Background(), metav1.ListOptions{})
   262  			errors.CheckError(err)
   263  			for _, secret := range secrets.Items {
   264  				if isArgoCDSecret(referencedSecrets, secret) {
   265  					pruneObjects[kube.ResourceKey{Group: "", Kind: "Secret", Name: secret.GetName()}] = secret
   266  				}
   267  			}
   268  			applications, err := acdClients.applications.List(context.Background(), metav1.ListOptions{})
   269  			errors.CheckError(err)
   270  			for _, app := range applications.Items {
   271  				pruneObjects[kube.ResourceKey{Group: "argoproj.io", Kind: "Application", Name: app.GetName()}] = app
   272  			}
   273  			projects, err := acdClients.projects.List(context.Background(), metav1.ListOptions{})
   274  			errors.CheckError(err)
   275  			for _, proj := range projects.Items {
   276  				pruneObjects[kube.ResourceKey{Group: "argoproj.io", Kind: "AppProject", Name: proj.GetName()}] = proj
   277  			}
   278  
   279  			// Create or replace existing object
   280  			backupObjects, err := kube.SplitYAML(input)
   281  			errors.CheckError(err)
   282  			for _, bakObj := range backupObjects {
   283  				gvk := bakObj.GroupVersionKind()
   284  				key := kube.ResourceKey{Group: gvk.Group, Kind: gvk.Kind, Name: bakObj.GetName()}
   285  				liveObj, exists := pruneObjects[key]
   286  				delete(pruneObjects, key)
   287  				var dynClient dynamic.ResourceInterface
   288  				switch bakObj.GetKind() {
   289  				case "Secret":
   290  					dynClient = acdClients.secrets
   291  				case "ConfigMap":
   292  					dynClient = acdClients.configMaps
   293  				case "AppProject":
   294  					dynClient = acdClients.projects
   295  				case "Application":
   296  					dynClient = acdClients.applications
   297  				}
   298  				if !exists {
   299  					if !dryRun {
   300  						_, err = dynClient.Create(context.Background(), bakObj, metav1.CreateOptions{})
   301  						errors.CheckError(err)
   302  					}
   303  					fmt.Printf("%s/%s %s created%s\n", gvk.Group, gvk.Kind, bakObj.GetName(), dryRunMsg)
   304  				} else if specsEqual(*bakObj, liveObj) {
   305  					fmt.Printf("%s/%s %s unchanged%s\n", gvk.Group, gvk.Kind, bakObj.GetName(), dryRunMsg)
   306  				} else {
   307  					if !dryRun {
   308  						newLive := updateLive(bakObj, &liveObj)
   309  						_, err = dynClient.Update(context.Background(), newLive, metav1.UpdateOptions{})
   310  						errors.CheckError(err)
   311  					}
   312  					fmt.Printf("%s/%s %s updated%s\n", gvk.Group, gvk.Kind, bakObj.GetName(), dryRunMsg)
   313  				}
   314  			}
   315  
   316  			// Delete objects not in backup
   317  			for key := range pruneObjects {
   318  				if prune {
   319  					var dynClient dynamic.ResourceInterface
   320  					switch key.Kind {
   321  					case "Secret":
   322  						dynClient = acdClients.secrets
   323  					case "AppProject":
   324  						dynClient = acdClients.projects
   325  					case "Application":
   326  						dynClient = acdClients.applications
   327  					default:
   328  						log.Fatalf("Unexpected kind '%s' in prune list", key.Kind)
   329  					}
   330  					if !dryRun {
   331  						err = dynClient.Delete(context.Background(), key.Name, metav1.DeleteOptions{})
   332  						errors.CheckError(err)
   333  					}
   334  					fmt.Printf("%s/%s %s pruned%s\n", key.Group, key.Kind, key.Name, dryRunMsg)
   335  				} else {
   336  					fmt.Printf("%s/%s %s needs pruning\n", key.Group, key.Kind, key.Name)
   337  				}
   338  			}
   339  		},
   340  	}
   341  
   342  	clientConfig = cli.AddKubectlFlagsToCmd(&command)
   343  	command.Flags().BoolVar(&dryRun, "dry-run", false, "Print what will be performed")
   344  	command.Flags().BoolVar(&prune, "prune", false, "Prune secrets, applications and projects which do not appear in the backup")
   345  
   346  	return &command
   347  }
   348  
   349  type argoCDClientsets struct {
   350  	configMaps   dynamic.ResourceInterface
   351  	secrets      dynamic.ResourceInterface
   352  	applications dynamic.ResourceInterface
   353  	projects     dynamic.ResourceInterface
   354  }
   355  
   356  func newArgoCDClientsets(config *rest.Config, namespace string) *argoCDClientsets {
   357  	dynamicIf, err := dynamic.NewForConfig(config)
   358  	errors.CheckError(err)
   359  	return &argoCDClientsets{
   360  		configMaps:   dynamicIf.Resource(configMapResource).Namespace(namespace),
   361  		secrets:      dynamicIf.Resource(secretResource).Namespace(namespace),
   362  		applications: dynamicIf.Resource(applicationsResource).Namespace(namespace),
   363  		projects:     dynamicIf.Resource(appprojectsResource).Namespace(namespace),
   364  	}
   365  }
   366  
   367  // NewExportCommand defines a new command for exporting Kubernetes and Argo CD resources.
   368  func NewExportCommand() *cobra.Command {
   369  	var (
   370  		clientConfig clientcmd.ClientConfig
   371  		out          string
   372  	)
   373  	var command = cobra.Command{
   374  		Use:   "export",
   375  		Short: "Export all Argo CD data to stdout (default) or a file",
   376  		Run: func(c *cobra.Command, args []string) {
   377  			config, err := clientConfig.ClientConfig()
   378  			errors.CheckError(err)
   379  			namespace, _, err := clientConfig.Namespace()
   380  			errors.CheckError(err)
   381  
   382  			var writer io.Writer
   383  			if out == "-" {
   384  				writer = os.Stdout
   385  			} else {
   386  				f, err := os.Create(out)
   387  				errors.CheckError(err)
   388  				bw := bufio.NewWriter(f)
   389  				writer = bw
   390  				defer func() {
   391  					err = bw.Flush()
   392  					errors.CheckError(err)
   393  					err = f.Close()
   394  					errors.CheckError(err)
   395  				}()
   396  			}
   397  
   398  			acdClients := newArgoCDClientsets(config, namespace)
   399  			acdConfigMap, err := acdClients.configMaps.Get(context.Background(), common.ArgoCDConfigMapName, metav1.GetOptions{})
   400  			errors.CheckError(err)
   401  			export(writer, *acdConfigMap)
   402  			acdRBACConfigMap, err := acdClients.configMaps.Get(context.Background(), common.ArgoCDRBACConfigMapName, metav1.GetOptions{})
   403  			errors.CheckError(err)
   404  			export(writer, *acdRBACConfigMap)
   405  			acdKnownHostsConfigMap, err := acdClients.configMaps.Get(context.Background(), common.ArgoCDKnownHostsConfigMapName, metav1.GetOptions{})
   406  			errors.CheckError(err)
   407  			export(writer, *acdKnownHostsConfigMap)
   408  			acdTLSCertsConfigMap, err := acdClients.configMaps.Get(context.Background(), common.ArgoCDTLSCertsConfigMapName, metav1.GetOptions{})
   409  			errors.CheckError(err)
   410  			export(writer, *acdTLSCertsConfigMap)
   411  
   412  			referencedSecrets := getReferencedSecrets(*acdConfigMap)
   413  			secrets, err := acdClients.secrets.List(context.Background(), metav1.ListOptions{})
   414  			errors.CheckError(err)
   415  			for _, secret := range secrets.Items {
   416  				if isArgoCDSecret(referencedSecrets, secret) {
   417  					export(writer, secret)
   418  				}
   419  			}
   420  			projects, err := acdClients.projects.List(context.Background(), metav1.ListOptions{})
   421  			errors.CheckError(err)
   422  			for _, proj := range projects.Items {
   423  				export(writer, proj)
   424  			}
   425  			applications, err := acdClients.applications.List(context.Background(), metav1.ListOptions{})
   426  			errors.CheckError(err)
   427  			for _, app := range applications.Items {
   428  				export(writer, app)
   429  			}
   430  		},
   431  	}
   432  
   433  	clientConfig = cli.AddKubectlFlagsToCmd(&command)
   434  	command.Flags().StringVarP(&out, "out", "o", "-", "Output to the specified file instead of stdout")
   435  
   436  	return &command
   437  }
   438  
   439  // getReferencedSecrets examines the argocd-cm config for any referenced repo secrets and returns a
   440  // map of all referenced secrets.
   441  func getReferencedSecrets(un unstructured.Unstructured) map[string]bool {
   442  	var cm apiv1.ConfigMap
   443  	err := runtime.DefaultUnstructuredConverter.FromUnstructured(un.Object, &cm)
   444  	errors.CheckError(err)
   445  	referencedSecrets := make(map[string]bool)
   446  
   447  	// Referenced repository secrets
   448  	if reposRAW, ok := cm.Data["repositories"]; ok {
   449  		repos := make([]settings.Repository, 0)
   450  		err := yaml.Unmarshal([]byte(reposRAW), &repos)
   451  		errors.CheckError(err)
   452  		for _, cred := range repos {
   453  			if cred.PasswordSecret != nil {
   454  				referencedSecrets[cred.PasswordSecret.Name] = true
   455  			}
   456  			if cred.SSHPrivateKeySecret != nil {
   457  				referencedSecrets[cred.SSHPrivateKeySecret.Name] = true
   458  			}
   459  			if cred.UsernameSecret != nil {
   460  				referencedSecrets[cred.UsernameSecret.Name] = true
   461  			}
   462  			if cred.TLSClientCertDataSecret != nil {
   463  				referencedSecrets[cred.TLSClientCertDataSecret.Name] = true
   464  			}
   465  			if cred.TLSClientCertKeySecret != nil {
   466  				referencedSecrets[cred.TLSClientCertKeySecret.Name] = true
   467  			}
   468  		}
   469  	}
   470  
   471  	// Referenced repository credentials secrets
   472  	if reposRAW, ok := cm.Data["repository.credentials"]; ok {
   473  		creds := make([]settings.RepositoryCredentials, 0)
   474  		err := yaml.Unmarshal([]byte(reposRAW), &creds)
   475  		errors.CheckError(err)
   476  		for _, cred := range creds {
   477  			if cred.PasswordSecret != nil {
   478  				referencedSecrets[cred.PasswordSecret.Name] = true
   479  			}
   480  			if cred.SSHPrivateKeySecret != nil {
   481  				referencedSecrets[cred.SSHPrivateKeySecret.Name] = true
   482  			}
   483  			if cred.UsernameSecret != nil {
   484  				referencedSecrets[cred.UsernameSecret.Name] = true
   485  			}
   486  			if cred.TLSClientCertDataSecret != nil {
   487  				referencedSecrets[cred.TLSClientCertDataSecret.Name] = true
   488  			}
   489  			if cred.TLSClientCertKeySecret != nil {
   490  				referencedSecrets[cred.TLSClientCertKeySecret.Name] = true
   491  			}
   492  		}
   493  	}
   494  	return referencedSecrets
   495  }
   496  
   497  // isArgoCDSecret returns whether or not the given secret is a part of Argo CD configuration
   498  // (e.g. argocd-secret, repo credentials, or cluster credentials)
   499  func isArgoCDSecret(repoSecretRefs map[string]bool, un unstructured.Unstructured) bool {
   500  	secretName := un.GetName()
   501  	if secretName == common.ArgoCDSecretName {
   502  		return true
   503  	}
   504  	if repoSecretRefs != nil {
   505  		if _, ok := repoSecretRefs[secretName]; ok {
   506  			return true
   507  		}
   508  	}
   509  	if labels := un.GetLabels(); labels != nil {
   510  		if _, ok := labels[common.LabelKeySecretType]; ok {
   511  			return true
   512  		}
   513  	}
   514  	if annotations := un.GetAnnotations(); annotations != nil {
   515  		if annotations[common.AnnotationKeyManagedBy] == common.AnnotationValueManagedByArgoCD {
   516  			return true
   517  		}
   518  	}
   519  	return false
   520  }
   521  
   522  // isArgoCDConfigMap returns true if the configmap name is one of argo cd's well known configmaps
   523  func isArgoCDConfigMap(name string) bool {
   524  	switch name {
   525  	case common.ArgoCDConfigMapName, common.ArgoCDRBACConfigMapName, common.ArgoCDKnownHostsConfigMapName, common.ArgoCDTLSCertsConfigMapName:
   526  		return true
   527  	}
   528  	return false
   529  
   530  }
   531  
   532  // specsEqual returns if the spec, data, labels, annotations, and finalizers of the two
   533  // supplied objects are equal, indicating that no update is necessary during importing
   534  func specsEqual(left, right unstructured.Unstructured) bool {
   535  	if !reflect.DeepEqual(left.GetAnnotations(), right.GetAnnotations()) {
   536  		return false
   537  	}
   538  	if !reflect.DeepEqual(left.GetLabels(), right.GetLabels()) {
   539  		return false
   540  	}
   541  	if !reflect.DeepEqual(left.GetFinalizers(), right.GetFinalizers()) {
   542  		return false
   543  	}
   544  	switch left.GetKind() {
   545  	case "Secret", "ConfigMap":
   546  		leftData, _, _ := unstructured.NestedMap(left.Object, "data")
   547  		rightData, _, _ := unstructured.NestedMap(right.Object, "data")
   548  		return reflect.DeepEqual(leftData, rightData)
   549  	case "AppProject":
   550  		leftSpec, _, _ := unstructured.NestedMap(left.Object, "spec")
   551  		rightSpec, _, _ := unstructured.NestedMap(right.Object, "spec")
   552  		return reflect.DeepEqual(leftSpec, rightSpec)
   553  	case "Application":
   554  		leftSpec, _, _ := unstructured.NestedMap(left.Object, "spec")
   555  		rightSpec, _, _ := unstructured.NestedMap(right.Object, "spec")
   556  		leftStatus, _, _ := unstructured.NestedMap(left.Object, "status")
   557  		rightStatus, _, _ := unstructured.NestedMap(right.Object, "status")
   558  		// reconciledAt and observedAt are constantly changing and we ignore any diff there
   559  		delete(leftStatus, "reconciledAt")
   560  		delete(rightStatus, "reconciledAt")
   561  		delete(leftStatus, "observedAt")
   562  		delete(rightStatus, "observedAt")
   563  		return reflect.DeepEqual(leftSpec, rightSpec) && reflect.DeepEqual(leftStatus, rightStatus)
   564  	}
   565  	return false
   566  }
   567  
   568  // updateLive replaces the live object's finalizers, spec, annotations, labels, and data from the
   569  // backup object but leaves all other fields intact (status, other metadata, etc...)
   570  func updateLive(bak, live *unstructured.Unstructured) *unstructured.Unstructured {
   571  	newLive := live.DeepCopy()
   572  	newLive.SetAnnotations(bak.GetAnnotations())
   573  	newLive.SetLabels(bak.GetLabels())
   574  	newLive.SetFinalizers(bak.GetFinalizers())
   575  	switch live.GetKind() {
   576  	case "Secret", "ConfigMap":
   577  		newLive.Object["data"] = bak.Object["data"]
   578  	case "AppProject":
   579  		newLive.Object["spec"] = bak.Object["spec"]
   580  	case "Application":
   581  		newLive.Object["spec"] = bak.Object["spec"]
   582  		if _, ok := bak.Object["status"]; ok {
   583  			newLive.Object["status"] = bak.Object["status"]
   584  		}
   585  	}
   586  	return newLive
   587  }
   588  
   589  // export writes the unstructured object and removes extraneous cruft from output before writing
   590  func export(w io.Writer, un unstructured.Unstructured) {
   591  	name := un.GetName()
   592  	finalizers := un.GetFinalizers()
   593  	apiVersion := un.GetAPIVersion()
   594  	kind := un.GetKind()
   595  	labels := un.GetLabels()
   596  	annotations := un.GetAnnotations()
   597  	unstructured.RemoveNestedField(un.Object, "metadata")
   598  	un.SetName(name)
   599  	un.SetFinalizers(finalizers)
   600  	un.SetAPIVersion(apiVersion)
   601  	un.SetKind(kind)
   602  	un.SetLabels(labels)
   603  	un.SetAnnotations(annotations)
   604  	data, err := yaml.Marshal(un.Object)
   605  	errors.CheckError(err)
   606  	_, err = w.Write(data)
   607  	errors.CheckError(err)
   608  	_, err = w.Write([]byte(yamlSeparator))
   609  	errors.CheckError(err)
   610  }
   611  
   612  // NewClusterConfig returns a new instance of `argocd-util kubeconfig` command
   613  func NewClusterConfig() *cobra.Command {
   614  	var (
   615  		clientConfig clientcmd.ClientConfig
   616  	)
   617  	var command = &cobra.Command{
   618  		Use:               "kubeconfig CLUSTER_URL OUTPUT_PATH",
   619  		Short:             "Generates kubeconfig for the specified cluster",
   620  		DisableAutoGenTag: true,
   621  		Run: func(c *cobra.Command, args []string) {
   622  			if len(args) != 2 {
   623  				c.HelpFunc()(c, args)
   624  				os.Exit(1)
   625  			}
   626  			serverUrl := args[0]
   627  			output := args[1]
   628  			conf, err := clientConfig.ClientConfig()
   629  			errors.CheckError(err)
   630  			namespace, _, err := clientConfig.Namespace()
   631  			errors.CheckError(err)
   632  			kubeclientset, err := kubernetes.NewForConfig(conf)
   633  			errors.CheckError(err)
   634  
   635  			cluster, err := db.NewDB(namespace, settings.NewSettingsManager(context.Background(), kubeclientset, namespace), kubeclientset).GetCluster(context.Background(), serverUrl)
   636  			errors.CheckError(err)
   637  			err = kube.WriteKubeConfig(cluster.RawRestConfig(), namespace, output)
   638  			errors.CheckError(err)
   639  		},
   640  	}
   641  	clientConfig = cli.AddKubectlFlagsToCmd(command)
   642  	return command
   643  }
   644  
   645  func iterateStringFields(obj interface{}, callback func(name string, val string) string) {
   646  	if mapField, ok := obj.(map[string]interface{}); ok {
   647  		for field, val := range mapField {
   648  			if strVal, ok := val.(string); ok {
   649  				mapField[field] = callback(field, strVal)
   650  			} else {
   651  				iterateStringFields(val, callback)
   652  			}
   653  		}
   654  	} else if arrayField, ok := obj.([]interface{}); ok {
   655  		for i := range arrayField {
   656  			iterateStringFields(arrayField[i], callback)
   657  		}
   658  	}
   659  }
   660  
   661  func redactor(dirtyString string) string {
   662  	config := make(map[string]interface{})
   663  	err := yaml.Unmarshal([]byte(dirtyString), &config)
   664  	errors.CheckError(err)
   665  	iterateStringFields(config, func(name string, val string) string {
   666  		if name == "clientSecret" || name == "secret" || name == "bindPW" {
   667  			return "********"
   668  		} else {
   669  			return val
   670  		}
   671  	})
   672  	data, err := yaml.Marshal(config)
   673  	errors.CheckError(err)
   674  	return string(data)
   675  }