github.com/argoproj/argo-cd/v3@v3.2.1/cmd/argocd/commands/admin/app.go (about) 1 package admin 2 3 import ( 4 "context" 5 "encoding/json" 6 stderrors "errors" 7 "fmt" 8 "net/http" 9 "os" 10 "sort" 11 "time" 12 13 "github.com/argoproj/gitops-engine/pkg/health" 14 "github.com/argoproj/gitops-engine/pkg/utils/kube" 15 "github.com/spf13/cobra" 16 corev1 "k8s.io/api/core/v1" 17 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 18 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 19 "k8s.io/apimachinery/pkg/util/runtime" 20 "k8s.io/client-go/kubernetes" 21 kubecache "k8s.io/client-go/tools/cache" 22 "k8s.io/client-go/tools/clientcmd" 23 "sigs.k8s.io/yaml" 24 25 cmdutil "github.com/argoproj/argo-cd/v3/cmd/util" 26 "github.com/argoproj/argo-cd/v3/common" 27 "github.com/argoproj/argo-cd/v3/controller" 28 "github.com/argoproj/argo-cd/v3/controller/cache" 29 "github.com/argoproj/argo-cd/v3/controller/metrics" 30 "github.com/argoproj/argo-cd/v3/controller/sharding" 31 argocdclient "github.com/argoproj/argo-cd/v3/pkg/apiclient" 32 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" 33 appclientset "github.com/argoproj/argo-cd/v3/pkg/client/clientset/versioned" 34 appinformers "github.com/argoproj/argo-cd/v3/pkg/client/informers/externalversions" 35 reposerverclient "github.com/argoproj/argo-cd/v3/reposerver/apiclient" 36 "github.com/argoproj/argo-cd/v3/util/argo" 37 "github.com/argoproj/argo-cd/v3/util/argo/normalizers" 38 cacheutil "github.com/argoproj/argo-cd/v3/util/cache" 39 appstatecache "github.com/argoproj/argo-cd/v3/util/cache/appstate" 40 "github.com/argoproj/argo-cd/v3/util/cli" 41 "github.com/argoproj/argo-cd/v3/util/config" 42 "github.com/argoproj/argo-cd/v3/util/db" 43 "github.com/argoproj/argo-cd/v3/util/errors" 44 utilio "github.com/argoproj/argo-cd/v3/util/io" 45 kubeutil "github.com/argoproj/argo-cd/v3/util/kube" 46 "github.com/argoproj/argo-cd/v3/util/settings" 47 ) 48 49 func NewAppCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command { 50 command := &cobra.Command{ 51 Use: "app", 52 Short: "Manage applications configuration", 53 Example: ` 54 # Compare results of two reconciliations and print diff 55 argocd admin app diff-reconcile-results APPNAME [flags] 56 57 # Generate declarative config for an application 58 argocd admin app generate-spec APPNAME 59 60 # Reconcile all applications and store reconciliation summary in the specified file 61 argocd admin app get-reconcile-results APPNAME 62 `, 63 Run: func(c *cobra.Command, args []string) { 64 c.HelpFunc()(c, args) 65 }, 66 } 67 68 command.AddCommand(NewGenAppSpecCommand()) 69 command.AddCommand(NewReconcileCommand(clientOpts)) 70 command.AddCommand(NewDiffReconcileResults()) 71 return command 72 } 73 74 // NewGenAppSpecCommand generates declarative configuration file for given application 75 func NewGenAppSpecCommand() *cobra.Command { 76 var ( 77 appOpts cmdutil.AppOptions 78 fileURL string 79 appName string 80 labels []string 81 outputFormat string 82 annotations []string 83 inline bool 84 setFinalizer bool 85 ) 86 command := &cobra.Command{ 87 Use: "generate-spec APPNAME", 88 Short: "Generate declarative config for an application", 89 Example: ` 90 # Generate declarative config for a directory app 91 argocd admin app generate-spec guestbook --repo https://github.com/argoproj/argocd-example-apps.git --path guestbook --dest-namespace default --dest-server https://kubernetes.default.svc --directory-recurse 92 93 # Generate declarative config for a Jsonnet app 94 argocd admin app generate-spec jsonnet-guestbook --repo https://github.com/argoproj/argocd-example-apps.git --path jsonnet-guestbook --dest-namespace default --dest-server https://kubernetes.default.svc --jsonnet-ext-str replicas=2 95 96 # Generate declarative config for a Helm app 97 argocd admin app generate-spec helm-guestbook --repo https://github.com/argoproj/argocd-example-apps.git --path helm-guestbook --dest-namespace default --dest-server https://kubernetes.default.svc --helm-set replicaCount=2 98 99 # Generate declarative config for a Helm app from a Helm repo 100 argocd admin app generate-spec nginx-ingress --repo https://charts.helm.sh/stable --helm-chart nginx-ingress --revision 1.24.3 --dest-namespace default --dest-server https://kubernetes.default.svc 101 102 # Generate declarative config for a Kustomize app 103 argocd admin app generate-spec kustomize-guestbook --repo https://github.com/argoproj/argocd-example-apps.git --path kustomize-guestbook --dest-namespace default --dest-server https://kubernetes.default.svc --kustomize-image quay.io/argoprojlabs/argocd-e2e-container:0.1 104 105 # Generate declarative config for a app using a custom tool: 106 argocd admin app generate-spec kasane --repo https://github.com/argoproj/argocd-example-apps.git --path plugins/kasane --dest-namespace default --dest-server https://kubernetes.default.svc --config-management-plugin kasane 107 `, 108 Run: func(c *cobra.Command, args []string) { 109 apps, err := cmdutil.ConstructApps(fileURL, appName, labels, annotations, args, appOpts, c.Flags()) 110 errors.CheckError(err) 111 if len(apps) > 1 { 112 errors.CheckError(stderrors.New("failed to generate spec, more than one application is not supported")) 113 } 114 app := apps[0] 115 if app.Name == "" { 116 c.HelpFunc()(c, args) 117 os.Exit(1) 118 } 119 if setFinalizer { 120 app.Finalizers = append(app.Finalizers, v1alpha1.ResourcesFinalizerName) 121 } 122 out, closer, err := getOutWriter(inline, fileURL) 123 errors.CheckError(err) 124 defer utilio.Close(closer) 125 126 errors.CheckError(PrintResources(outputFormat, out, app)) 127 }, 128 } 129 command.Flags().StringVar(&appName, "name", "", "A name for the app, ignored if a file is set (DEPRECATED)") 130 command.Flags().StringVarP(&fileURL, "file", "f", "", "Filename or URL to Kubernetes manifests for the app") 131 command.Flags().StringArrayVarP(&labels, "label", "l", []string{}, "Labels to apply to the app") 132 command.Flags().StringArrayVarP(&annotations, "annotations", "", []string{}, "Set metadata annotations (e.g. example=value)") 133 command.Flags().StringVarP(&outputFormat, "output", "o", "yaml", "Output format. One of: json|yaml") 134 command.Flags().BoolVarP(&inline, "inline", "i", false, "If set then generated resource is written back to the file specified in --file flag") 135 command.Flags().BoolVar(&setFinalizer, "set-finalizer", false, "Sets deletion finalizer on the application, application resources will be cascaded on deletion") 136 137 // Only complete files with appropriate extension. 138 err := command.Flags().SetAnnotation("file", cobra.BashCompFilenameExt, []string{"json", "yaml", "yml"}) 139 errors.CheckError(err) 140 141 cmdutil.AddAppFlags(command, &appOpts) 142 return command 143 } 144 145 type appReconcileResult struct { 146 Name string `json:"name"` 147 Health health.HealthStatusCode `json:"health"` 148 Sync *v1alpha1.SyncStatus `json:"sync"` 149 Conditions []v1alpha1.ApplicationCondition `json:"conditions"` 150 } 151 152 type reconcileResults struct { 153 Applications []appReconcileResult `json:"applications"` 154 } 155 156 func (r *reconcileResults) getAppsMap() map[string]appReconcileResult { 157 res := map[string]appReconcileResult{} 158 for i := range r.Applications { 159 res[r.Applications[i].Name] = r.Applications[i] 160 } 161 return res 162 } 163 164 func printLine(format string, a ...any) { 165 _, _ = fmt.Printf(format+"\n", a...) 166 } 167 168 func NewDiffReconcileResults() *cobra.Command { 169 command := &cobra.Command{ 170 Use: "diff-reconcile-results PATH1 PATH2", 171 Short: "Compare results of two reconciliations and print diff.", 172 Run: func(c *cobra.Command, args []string) { 173 if len(args) != 2 { 174 c.HelpFunc()(c, args) 175 os.Exit(1) 176 } 177 178 path1 := args[0] 179 path2 := args[1] 180 var res1 reconcileResults 181 var res2 reconcileResults 182 errors.CheckError(config.UnmarshalLocalFile(path1, &res1)) 183 errors.CheckError(config.UnmarshalLocalFile(path2, &res2)) 184 errors.CheckError(diffReconcileResults(res1, res2)) 185 }, 186 } 187 188 return command 189 } 190 191 func toUnstructured(val any) (*unstructured.Unstructured, error) { 192 data, err := json.Marshal(val) 193 if err != nil { 194 return nil, fmt.Errorf("error while marhsalling value: %w", err) 195 } 196 res := make(map[string]any) 197 err = json.Unmarshal(data, &res) 198 if err != nil { 199 return nil, fmt.Errorf("error while unmarhsalling data: %w", err) 200 } 201 return &unstructured.Unstructured{Object: res}, nil 202 } 203 204 type diffPair struct { 205 name string 206 first *unstructured.Unstructured 207 second *unstructured.Unstructured 208 } 209 210 func diffReconcileResults(res1 reconcileResults, res2 reconcileResults) error { 211 var pairs []diffPair 212 resMap1 := res1.getAppsMap() 213 resMap2 := res2.getAppsMap() 214 for k, v := range resMap1 { 215 firstUn, err := toUnstructured(v) 216 if err != nil { 217 return fmt.Errorf("error converting first resource to unstructured: %w", err) 218 } 219 var secondUn *unstructured.Unstructured 220 second, ok := resMap2[k] 221 if ok { 222 secondUn, err = toUnstructured(second) 223 if err != nil { 224 return fmt.Errorf("error converting second resource to unstructured: %w", err) 225 } 226 delete(resMap2, k) 227 } 228 pairs = append(pairs, diffPair{name: k, first: firstUn, second: secondUn}) 229 } 230 for k, v := range resMap2 { 231 secondUn, err := toUnstructured(v) 232 if err != nil { 233 return fmt.Errorf("error converting second resource of second map to unstructure: %w", err) 234 } 235 pairs = append(pairs, diffPair{name: k, first: nil, second: secondUn}) 236 } 237 sort.Slice(pairs, func(i, j int) bool { 238 return pairs[i].name < pairs[j].name 239 }) 240 for _, item := range pairs { 241 printLine(item.name) 242 _ = cli.PrintDiff(item.name, item.first, item.second) 243 } 244 245 return nil 246 } 247 248 func NewReconcileCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command { 249 var ( 250 clientConfig clientcmd.ClientConfig 251 selector string 252 repoServerAddress string 253 outputFormat string 254 refresh bool 255 serverSideDiff bool 256 ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts 257 ) 258 259 command := &cobra.Command{ 260 Use: "get-reconcile-results PATH", 261 Short: "Reconcile all applications and stores reconciliation summary in the specified file.", 262 Run: func(c *cobra.Command, args []string) { 263 ctx := c.Context() 264 265 // get rid of logging error handler 266 runtime.ErrorHandlers = runtime.ErrorHandlers[1:] 267 268 if len(args) != 1 { 269 c.HelpFunc()(c, args) 270 os.Exit(1) 271 } 272 outputPath := args[0] 273 274 errors.CheckError(os.Setenv(v1alpha1.EnvVarFakeInClusterConfig, "true")) 275 cfg, err := clientConfig.ClientConfig() 276 errors.CheckError(err) 277 namespace, _, err := clientConfig.Namespace() 278 errors.CheckError(err) 279 280 var result []appReconcileResult 281 if refresh { 282 appClientset := appclientset.NewForConfigOrDie(cfg) 283 kubeClientset := kubernetes.NewForConfigOrDie(cfg) 284 if repoServerAddress == "" { 285 printLine("Repo server is not provided, trying to port-forward to argocd-repo-server pod.") 286 overrides := clientcmd.ConfigOverrides{} 287 repoServerName := clientOpts.RepoServerName 288 repoServerServiceLabelSelector := common.LabelKeyComponentRepoServer + "=" + common.LabelValueComponentRepoServer 289 repoServerServices, err := kubeClientset.CoreV1().Services(namespace).List(context.Background(), metav1.ListOptions{LabelSelector: repoServerServiceLabelSelector}) 290 errors.CheckError(err) 291 if len(repoServerServices.Items) > 0 { 292 if repoServerServicelabel, ok := repoServerServices.Items[0].Labels[common.LabelKeyAppName]; ok && repoServerServicelabel != "" { 293 repoServerName = repoServerServicelabel 294 } 295 } 296 repoServerPodLabelSelector := common.LabelKeyAppName + "=" + repoServerName 297 repoServerPort, err := kubeutil.PortForward(8081, namespace, &overrides, repoServerPodLabelSelector) 298 errors.CheckError(err) 299 repoServerAddress = fmt.Sprintf("localhost:%d", repoServerPort) 300 } 301 repoServerClient := reposerverclient.NewRepoServerClientset(repoServerAddress, 60, reposerverclient.TLSConfiguration{DisableTLS: false, StrictValidation: false}) 302 result, err = reconcileApplications(ctx, kubeClientset, appClientset, namespace, repoServerClient, selector, newLiveStateCache, serverSideDiff, ignoreNormalizerOpts) 303 errors.CheckError(err) 304 } else { 305 appClientset := appclientset.NewForConfigOrDie(cfg) 306 result, err = getReconcileResults(ctx, appClientset, namespace, selector) 307 } 308 309 errors.CheckError(saveToFile(err, outputFormat, reconcileResults{Applications: result}, outputPath)) 310 }, 311 } 312 clientConfig = cli.AddKubectlFlagsToCmd(command) 313 command.Flags().StringVar(&repoServerAddress, "repo-server", "", "Repo server address.") 314 command.Flags().StringVar(&selector, "l", "", "Label selector") 315 command.Flags().StringVar(&outputFormat, "o", "yaml", "Output format (yaml|json)") 316 command.Flags().BoolVar(&refresh, "refresh", false, "If set to true then recalculates apps reconciliation") 317 command.Flags().BoolVar(&serverSideDiff, "server-side-diff", false, "If set to \"true\" will use server-side diff while comparing resources. Default (\"false\")") 318 command.Flags().DurationVar(&ignoreNormalizerOpts.JQExecutionTimeout, "ignore-normalizer-jq-execution-timeout", normalizers.DefaultJQExecutionTimeout, "Set ignore normalizer JQ execution timeout") 319 return command 320 } 321 322 func saveToFile(err error, outputFormat string, result reconcileResults, outputPath string) error { 323 errors.CheckError(err) 324 var data []byte 325 switch outputFormat { 326 case "yaml": 327 if data, err = yaml.Marshal(result); err != nil { 328 return fmt.Errorf("error marshalling yaml: %w", err) 329 } 330 case "json": 331 if data, err = json.Marshal(result); err != nil { 332 return fmt.Errorf("error marshalling json: %w", err) 333 } 334 default: 335 return fmt.Errorf("format %s is not supported", outputFormat) 336 } 337 338 return os.WriteFile(outputPath, data, 0o644) 339 } 340 341 func getReconcileResults(ctx context.Context, appClientset appclientset.Interface, namespace string, selector string) ([]appReconcileResult, error) { 342 appsList, err := appClientset.ArgoprojV1alpha1().Applications(namespace).List(ctx, metav1.ListOptions{LabelSelector: selector}) 343 if err != nil { 344 return nil, fmt.Errorf("error listing namespaced apps: %w", err) 345 } 346 347 var items []appReconcileResult 348 for _, app := range appsList.Items { 349 items = append(items, appReconcileResult{ 350 Name: app.Name, 351 Conditions: app.Status.Conditions, 352 Health: app.Status.Health.Status, 353 Sync: &app.Status.Sync, 354 }) 355 } 356 return items, nil 357 } 358 359 func reconcileApplications( 360 ctx context.Context, 361 kubeClientset kubernetes.Interface, 362 appClientset appclientset.Interface, 363 namespace string, 364 repoServerClient reposerverclient.Clientset, 365 selector string, 366 createLiveStateCache func(argoDB db.ArgoDB, appInformer kubecache.SharedIndexInformer, settingsMgr *settings.SettingsManager, server *metrics.MetricsServer) cache.LiveStateCache, 367 serverSideDiff bool, 368 ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts, 369 ) ([]appReconcileResult, error) { 370 settingsMgr := settings.NewSettingsManager(ctx, kubeClientset, namespace) 371 argoDB := db.NewDB(namespace, settingsMgr, kubeClientset) 372 appInformerFactory := appinformers.NewSharedInformerFactoryWithOptions( 373 appClientset, 374 1*time.Hour, 375 appinformers.WithNamespace(namespace), 376 appinformers.WithTweakListOptions(func(_ *metav1.ListOptions) {}), 377 ) 378 379 appInformer := appInformerFactory.Argoproj().V1alpha1().Applications().Informer() 380 projInformer := appInformerFactory.Argoproj().V1alpha1().AppProjects().Informer() 381 go appInformer.Run(ctx.Done()) 382 go projInformer.Run(ctx.Done()) 383 if !kubecache.WaitForCacheSync(ctx.Done(), appInformer.HasSynced, projInformer.HasSynced) { 384 return nil, stderrors.New("failed to sync cache") 385 } 386 387 appLister := appInformerFactory.Argoproj().V1alpha1().Applications().Lister() 388 projLister := appInformerFactory.Argoproj().V1alpha1().AppProjects().Lister() 389 server, err := metrics.NewMetricsServer("", appLister, func(_ any) bool { 390 return true 391 }, func(_ *http.Request) error { 392 return nil 393 }, []string{}, []string{}, argoDB) 394 if err != nil { 395 return nil, fmt.Errorf("error starting new metrics server: %w", err) 396 } 397 stateCache := createLiveStateCache(argoDB, appInformer, settingsMgr, server) 398 if err := stateCache.Init(); err != nil { 399 return nil, fmt.Errorf("error initializing state cache: %w", err) 400 } 401 402 cache := appstatecache.NewCache( 403 cacheutil.NewCache(cacheutil.NewInMemoryCache(1*time.Minute)), 404 1*time.Minute, 405 ) 406 407 appStateManager := controller.NewAppStateManager( 408 argoDB, 409 appClientset, 410 repoServerClient, 411 namespace, 412 kubeutil.NewKubectl(), 413 func(_ string) (kube.CleanupFunc, error) { 414 return func() {}, nil 415 }, 416 settingsMgr, 417 stateCache, 418 server, 419 cache, 420 time.Second, 421 argo.NewResourceTracking(), 422 false, 423 0, 424 serverSideDiff, 425 ignoreNormalizerOpts, 426 ) 427 428 appsList, err := appClientset.ArgoprojV1alpha1().Applications(namespace).List(ctx, metav1.ListOptions{LabelSelector: selector}) 429 if err != nil { 430 return nil, fmt.Errorf("error listing namespaced apps: %w", err) 431 } 432 433 sort.Slice(appsList.Items, func(i, j int) bool { 434 return appsList.Items[i].Spec.Destination.Server < appsList.Items[j].Spec.Destination.Server 435 }) 436 437 var items []appReconcileResult 438 prevServer := "" 439 for _, app := range appsList.Items { 440 destCluster, err := argo.GetDestinationCluster(ctx, app.Spec.Destination, argoDB) 441 if err != nil { 442 return nil, fmt.Errorf("error getting destination cluster: %w", err) 443 } 444 445 if prevServer != destCluster.Server { 446 if prevServer != "" { 447 if clusterCache, err := stateCache.GetClusterCache(destCluster); err == nil { 448 clusterCache.Invalidate() 449 } 450 } 451 printLine("Reconciling apps of %s", destCluster.Server) 452 prevServer = destCluster.Server 453 } 454 printLine(app.Name) 455 456 proj, err := projLister.AppProjects(namespace).Get(app.Spec.Project) 457 if err != nil { 458 return nil, fmt.Errorf("error getting namespaced project: %w", err) 459 } 460 461 sources := make([]v1alpha1.ApplicationSource, 0) 462 revisions := make([]string, 0) 463 sources = append(sources, app.Spec.GetSource()) 464 revisions = append(revisions, app.Spec.GetSource().TargetRevision) 465 466 res, err := appStateManager.CompareAppState(&app, proj, revisions, sources, false, false, nil, false) 467 if err != nil { 468 return nil, fmt.Errorf("error comparing app states: %w", err) 469 } 470 items = append(items, appReconcileResult{ 471 Name: app.Name, 472 Conditions: app.Status.Conditions, 473 Health: res.GetHealthStatus(), 474 Sync: res.GetSyncStatus(), 475 }) 476 } 477 return items, nil 478 } 479 480 func newLiveStateCache(argoDB db.ArgoDB, appInformer kubecache.SharedIndexInformer, settingsMgr *settings.SettingsManager, server *metrics.MetricsServer) cache.LiveStateCache { 481 return cache.NewLiveStateCache(argoDB, appInformer, settingsMgr, server, func(_ map[string]bool, _ corev1.ObjectReference) {}, &sharding.ClusterSharding{}, argo.NewResourceTracking()) 482 }