@@ -39,9 +39,13 @@ import (
3939 "github.com/argoproj/argo-cd/v3/cmd/argocd/commands/headless"
4040 "github.com/argoproj/argo-cd/v3/cmd/argocd/commands/utils"
4141 cmdutil "github.com/argoproj/argo-cd/v3/cmd/util"
42+ argocommon "github.com/argoproj/argo-cd/v3/common"
4243 "github.com/argoproj/argo-cd/v3/controller"
4344 argocdclient "github.com/argoproj/argo-cd/v3/pkg/apiclient"
4445 "github.com/argoproj/argo-cd/v3/pkg/apiclient/application"
46+
47+ resourceutil "github.com/argoproj/gitops-engine/pkg/sync/resource"
48+
4549 clusterpkg "github.com/argoproj/argo-cd/v3/pkg/apiclient/cluster"
4650 projectpkg "github.com/argoproj/argo-cd/v3/pkg/apiclient/project"
4751 "github.com/argoproj/argo-cd/v3/pkg/apiclient/settings"
@@ -1282,6 +1286,7 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
12821286 revision string
12831287 localRepoRoot string
12841288 serverSideGenerate bool
1289+ serverSideDiff bool
12851290 localIncludes []string
12861291 appNamespace string
12871292 revisions []string
@@ -1344,6 +1349,22 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
13441349 argoSettings , err := settingsIf .Get (ctx , & settings.SettingsQuery {})
13451350 errors .CheckError (err )
13461351 diffOption := & DifferenceOption {}
1352+
1353+ hasServerSideDiffAnnotation := resourceutil .HasAnnotationOption (app , argocommon .AnnotationCompareOptions , "ServerSideDiff=true" )
1354+
1355+ // Use annotation if flag not explicitly set
1356+ if ! c .Flags ().Changed ("server-side-diff" ) {
1357+ serverSideDiff = hasServerSideDiffAnnotation
1358+ } else if serverSideDiff && ! hasServerSideDiffAnnotation {
1359+ // Flag explicitly set to true, but app annotation is not set
1360+ fmt .Fprintf (os .Stderr , "Warning: Application does not have ServerSideDiff=true annotation.\n " )
1361+ }
1362+
1363+ // Server side diff with local requires server side generate to be set as there will be a mismatch with client-generated manifests.
1364+ if serverSideDiff && local != "" && ! serverSideGenerate {
1365+ log .Fatal ("--server-side-diff with --local requires --server-side-generate." )
1366+ }
1367+
13471368 switch {
13481369 case app .Spec .HasMultipleSources () && len (revisions ) > 0 && len (sourcePositions ) > 0 :
13491370 numOfSources := int64 (len (app .Spec .GetSources ()))
@@ -1399,7 +1420,8 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
13991420 }
14001421 }
14011422 proj := getProject (ctx , c , clientOpts , app .Spec .Project )
1402- foundDiffs := findandPrintDiff (ctx , app , proj .Project , resources , argoSettings , diffOption , ignoreNormalizerOpts )
1423+
1424+ foundDiffs := findAndPrintDiff (ctx , app , proj .Project , resources , argoSettings , diffOption , ignoreNormalizerOpts , serverSideDiff , appIf , app .GetName (), app .GetNamespace ())
14031425 if foundDiffs && exitCode {
14041426 os .Exit (diffExitCode )
14051427 }
@@ -1413,6 +1435,7 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
14131435 command .Flags ().StringVar (& revision , "revision" , "" , "Compare live app to a particular revision" )
14141436 command .Flags ().StringVar (& localRepoRoot , "local-repo-root" , "/" , "Path to the repository root. Used together with --local allows setting the repository root" )
14151437 command .Flags ().BoolVar (& serverSideGenerate , "server-side-generate" , false , "Used with --local, this will send your manifests to the server for diffing" )
1438+ command .Flags ().BoolVar (& serverSideDiff , "server-side-diff" , false , "Use server-side diff to calculate the diff. This will default to true if the ServerSideDiff annotation is set on the application." )
14161439 command .Flags ().StringArrayVar (& localIncludes , "local-include" , []string {"*.yaml" , "*.yml" , "*.json" }, "Used with --server-side-generate, specify patterns of filenames to send. Matching is based on filename and not path." )
14171440 command .Flags ().StringVarP (& appNamespace , "app-namespace" , "N" , "" , "Only render the difference in namespace" )
14181441 command .Flags ().StringArrayVar (& revisions , "revisions" , []string {}, "Show manifests at specific revisions for source position in source-positions" )
@@ -1422,6 +1445,101 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
14221445 return command
14231446}
14241447
1448+ // printResourceDiff prints the diff header and calls cli.PrintDiff for a resource
1449+ func printResourceDiff (group , kind , namespace , name string , live , target * unstructured.Unstructured ) {
1450+ fmt .Printf ("\n ===== %s/%s %s/%s ======\n " , group , kind , namespace , name )
1451+ _ = cli .PrintDiff (name , live , target )
1452+ }
1453+
1454+ // findAndPrintServerSideDiff performs a server-side diff by making requests to the api server and prints the response
1455+ func findAndPrintServerSideDiff (ctx context.Context , app * argoappv1.Application , items []objKeyLiveTarget , resources * application.ManagedResourcesResponse , appIf application.ApplicationServiceClient , appName , appNs string ) bool {
1456+ // Process each item for server-side diff
1457+ foundDiffs := false
1458+ for _ , item := range items {
1459+ if item .target != nil && hook .IsHook (item .target ) || item .live != nil && hook .IsHook (item .live ) {
1460+ continue
1461+ }
1462+
1463+ // For server-side diff, we need to create aligned arrays for this specific resource
1464+ var liveResource * argoappv1.ResourceDiff
1465+ var targetManifest string
1466+
1467+ if item .live != nil {
1468+ for _ , res := range resources .Items {
1469+ if res .Group == item .key .Group && res .Kind == item .key .Kind &&
1470+ res .Namespace == item .key .Namespace && res .Name == item .key .Name {
1471+ liveResource = res
1472+ break
1473+ }
1474+ }
1475+ }
1476+
1477+ if liveResource == nil {
1478+ // Create empty live resource for creation case
1479+ liveResource = & argoappv1.ResourceDiff {
1480+ Group : item .key .Group ,
1481+ Kind : item .key .Kind ,
1482+ Namespace : item .key .Namespace ,
1483+ Name : item .key .Name ,
1484+ LiveState : "" ,
1485+ TargetState : "" ,
1486+ Modified : true ,
1487+ }
1488+ }
1489+
1490+ if item .target != nil {
1491+ jsonBytes , err := json .Marshal (item .target )
1492+ if err != nil {
1493+ errors .CheckError (fmt .Errorf ("error marshaling target object: %w" , err ))
1494+ }
1495+ targetManifest = string (jsonBytes )
1496+ }
1497+
1498+ // Call server-side diff for this individual resource
1499+ serverSideDiffQuery := & application.ApplicationServerSideDiffQuery {
1500+ AppName : & appName ,
1501+ AppNamespace : & appNs ,
1502+ Project : & app .Spec .Project ,
1503+ LiveResources : []* argoappv1.ResourceDiff {liveResource },
1504+ TargetManifests : []string {targetManifest },
1505+ }
1506+
1507+ serverSideDiffRes , err := appIf .ServerSideDiff (ctx , serverSideDiffQuery )
1508+ if err != nil {
1509+ errors .CheckError (err )
1510+ }
1511+
1512+ // Extract diff for this resource
1513+ for _ , resultItem := range serverSideDiffRes .Items {
1514+ if resultItem .Hook || (! resultItem .Modified && resultItem .TargetState != "" && resultItem .LiveState != "" ) {
1515+ continue
1516+ }
1517+
1518+ if resultItem .Modified || resultItem .TargetState == "" || resultItem .LiveState == "" {
1519+ var live , target * unstructured.Unstructured
1520+
1521+ if resultItem .TargetState != "" && resultItem .TargetState != "null" {
1522+ target = & unstructured.Unstructured {}
1523+ err = json .Unmarshal ([]byte (resultItem .TargetState ), target )
1524+ errors .CheckError (err )
1525+ }
1526+
1527+ if resultItem .LiveState != "" && resultItem .LiveState != "null" {
1528+ live = & unstructured.Unstructured {}
1529+ err = json .Unmarshal ([]byte (resultItem .LiveState ), live )
1530+ errors .CheckError (err )
1531+ }
1532+
1533+ // Print resulting diff for this resource
1534+ foundDiffs = true
1535+ printResourceDiff (resultItem .Group , resultItem .Kind , resultItem .Namespace , resultItem .Name , live , target )
1536+ }
1537+ }
1538+ }
1539+
1540+ return foundDiffs
1541+ }
1542+
14251543// DifferenceOption struct to store diff options
14261544type DifferenceOption struct {
14271545 local string
@@ -1433,47 +1551,15 @@ type DifferenceOption struct {
14331551 revisions []string
14341552}
14351553
1436- // findandPrintDiff ... Prints difference between application current state and state stored in git or locally, returns boolean as true if difference is found else returns false
1437- func findandPrintDiff (ctx context.Context , app * argoappv1.Application , proj * argoappv1.AppProject , resources * application.ManagedResourcesResponse , argoSettings * settings.Settings , diffOptions * DifferenceOption , ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts ) bool {
1554+ // findAndPrintDiff ... Prints difference between application current state and state stored in git or locally, returns boolean as true if difference is found else returns false
1555+ func findAndPrintDiff (ctx context.Context , app * argoappv1.Application , proj * argoappv1.AppProject , resources * application.ManagedResourcesResponse , argoSettings * settings.Settings , diffOptions * DifferenceOption , ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts , useServerSideDiff bool , appIf application. ApplicationServiceClient , appName , appNs string ) bool {
14381556 var foundDiffs bool
1439- liveObjs , err := cmdutil .LiveObjects (resources .Items )
1440- errors .CheckError (err )
1441- items := make ([]objKeyLiveTarget , 0 )
1442- switch {
1443- case diffOptions .local != "" :
1444- localObjs := groupObjsByKey (getLocalObjects (ctx , app , proj , diffOptions .local , diffOptions .localRepoRoot , argoSettings .AppLabelKey , diffOptions .cluster .Info .ServerVersion , diffOptions .cluster .Info .APIVersions , argoSettings .KustomizeOptions , argoSettings .TrackingMethod ), liveObjs , app .Spec .Destination .Namespace )
1445- items = groupObjsForDiff (resources , localObjs , items , argoSettings , app .InstanceName (argoSettings .ControllerNamespace ), app .Spec .Destination .Namespace )
1446- case diffOptions .revision != "" || len (diffOptions .revisions ) > 0 :
1447- var unstructureds []* unstructured.Unstructured
1448- for _ , mfst := range diffOptions .res .Manifests {
1449- obj , err := argoappv1 .UnmarshalToUnstructured (mfst )
1450- errors .CheckError (err )
1451- unstructureds = append (unstructureds , obj )
1452- }
1453- groupedObjs := groupObjsByKey (unstructureds , liveObjs , app .Spec .Destination .Namespace )
1454- items = groupObjsForDiff (resources , groupedObjs , items , argoSettings , app .InstanceName (argoSettings .ControllerNamespace ), app .Spec .Destination .Namespace )
1455- case diffOptions .serversideRes != nil :
1456- var unstructureds []* unstructured.Unstructured
1457- for _ , mfst := range diffOptions .serversideRes .Manifests {
1458- obj , err := argoappv1 .UnmarshalToUnstructured (mfst )
1459- errors .CheckError (err )
1460- unstructureds = append (unstructureds , obj )
1461- }
1462- groupedObjs := groupObjsByKey (unstructureds , liveObjs , app .Spec .Destination .Namespace )
1463- items = groupObjsForDiff (resources , groupedObjs , items , argoSettings , app .InstanceName (argoSettings .ControllerNamespace ), app .Spec .Destination .Namespace )
1464- default :
1465- for i := range resources .Items {
1466- res := resources .Items [i ]
1467- live := & unstructured.Unstructured {}
1468- err := json .Unmarshal ([]byte (res .NormalizedLiveState ), & live )
1469- errors .CheckError (err )
14701557
1471- target := & unstructured.Unstructured {}
1472- err = json .Unmarshal ([]byte (res .TargetState ), & target )
1473- errors .CheckError (err )
1558+ items , err := prepareObjectsForDiff (ctx , app , proj , resources , argoSettings , diffOptions )
1559+ errors .CheckError (err )
14741560
1475- items = append ( items , objKeyLiveTarget { kube . NewResourceKey ( res . Group , res . Kind , res . Namespace , res . Name ), live , target })
1476- }
1561+ if useServerSideDiff {
1562+ return findAndPrintServerSideDiff ( ctx , app , items , resources , appIf , appName , appNs )
14771563 }
14781564
14791565 for _ , item := range items {
@@ -1500,7 +1586,6 @@ func findandPrintDiff(ctx context.Context, app *argoappv1.Application, proj *arg
15001586 errors .CheckError (err )
15011587
15021588 if diffRes .Modified || item .target == nil || item .live == nil {
1503- fmt .Printf ("\n ===== %s/%s %s/%s ======\n " , item .key .Group , item .key .Kind , item .key .Namespace , item .key .Name )
15041589 var live * unstructured.Unstructured
15051590 var target * unstructured.Unstructured
15061591 if item .target != nil && item .live != nil {
@@ -1512,10 +1597,8 @@ func findandPrintDiff(ctx context.Context, app *argoappv1.Application, proj *arg
15121597 live = item .live
15131598 target = item .target
15141599 }
1515- if ! foundDiffs {
1516- foundDiffs = true
1517- }
1518- _ = cli .PrintDiff (item .key .Name , live , target )
1600+ foundDiffs = true
1601+ printResourceDiff (item .key .Group , item .key .Kind , item .key .Namespace , item .key .Name , live , target )
15191602 }
15201603 }
15211604 return foundDiffs
@@ -2297,7 +2380,11 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
22972380 fmt .Printf ("====== Previewing differences between live and desired state of application %s ======\n " , appQualifiedName )
22982381
22992382 proj := getProject (ctx , c , clientOpts , app .Spec .Project )
2300- foundDiffs = findandPrintDiff (ctx , app , proj .Project , resources , argoSettings , diffOption , ignoreNormalizerOpts )
2383+
2384+ // Check if application has ServerSideDiff annotation
2385+ serverSideDiff := resourceutil .HasAnnotationOption (app , argocommon .AnnotationCompareOptions , "ServerSideDiff=true" )
2386+
2387+ foundDiffs = findAndPrintDiff (ctx , app , proj .Project , resources , argoSettings , diffOption , ignoreNormalizerOpts , serverSideDiff , appIf , appName , appNs )
23012388 if ! foundDiffs {
23022389 fmt .Printf ("====== No Differences found ======\n " )
23032390 // if no differences found, then no need to sync
@@ -3520,3 +3607,60 @@ func NewApplicationConfirmDeletionCommand(clientOpts *argocdclient.ClientOptions
35203607 command .Flags ().StringVarP (& appNamespace , "app-namespace" , "N" , "" , "Namespace of the target application where the source will be appended" )
35213608 return command
35223609}
3610+
3611+ // prepareObjectsForDiff prepares objects for diffing using the switch statement
3612+ // to handle different diff options and building the objKeyLiveTarget items
3613+ func prepareObjectsForDiff (ctx context.Context , app * argoappv1.Application , proj * argoappv1.AppProject , resources * application.ManagedResourcesResponse , argoSettings * settings.Settings , diffOptions * DifferenceOption ) ([]objKeyLiveTarget , error ) {
3614+ liveObjs , err := cmdutil .LiveObjects (resources .Items )
3615+ if err != nil {
3616+ return nil , err
3617+ }
3618+ items := make ([]objKeyLiveTarget , 0 )
3619+
3620+ switch {
3621+ case diffOptions .local != "" :
3622+ localObjs := groupObjsByKey (getLocalObjects (ctx , app , proj , diffOptions .local , diffOptions .localRepoRoot , argoSettings .AppLabelKey , diffOptions .cluster .Info .ServerVersion , diffOptions .cluster .Info .APIVersions , argoSettings .KustomizeOptions , argoSettings .TrackingMethod ), liveObjs , app .Spec .Destination .Namespace )
3623+ items = groupObjsForDiff (resources , localObjs , items , argoSettings , app .InstanceName (argoSettings .ControllerNamespace ), app .Spec .Destination .Namespace )
3624+ case diffOptions .revision != "" || len (diffOptions .revisions ) > 0 :
3625+ var unstructureds []* unstructured.Unstructured
3626+ for _ , mfst := range diffOptions .res .Manifests {
3627+ obj , err := argoappv1 .UnmarshalToUnstructured (mfst )
3628+ if err != nil {
3629+ return nil , err
3630+ }
3631+ unstructureds = append (unstructureds , obj )
3632+ }
3633+ groupedObjs := groupObjsByKey (unstructureds , liveObjs , app .Spec .Destination .Namespace )
3634+ items = groupObjsForDiff (resources , groupedObjs , items , argoSettings , app .InstanceName (argoSettings .ControllerNamespace ), app .Spec .Destination .Namespace )
3635+ case diffOptions .serversideRes != nil :
3636+ var unstructureds []* unstructured.Unstructured
3637+ for _ , mfst := range diffOptions .serversideRes .Manifests {
3638+ obj , err := argoappv1 .UnmarshalToUnstructured (mfst )
3639+ if err != nil {
3640+ return nil , err
3641+ }
3642+ unstructureds = append (unstructureds , obj )
3643+ }
3644+ groupedObjs := groupObjsByKey (unstructureds , liveObjs , app .Spec .Destination .Namespace )
3645+ items = groupObjsForDiff (resources , groupedObjs , items , argoSettings , app .InstanceName (argoSettings .ControllerNamespace ), app .Spec .Destination .Namespace )
3646+ default :
3647+ for i := range resources .Items {
3648+ res := resources .Items [i ]
3649+ live := & unstructured.Unstructured {}
3650+ err := json .Unmarshal ([]byte (res .NormalizedLiveState ), & live )
3651+ if err != nil {
3652+ return nil , err
3653+ }
3654+
3655+ target := & unstructured.Unstructured {}
3656+ err = json .Unmarshal ([]byte (res .TargetState ), & target )
3657+ if err != nil {
3658+ return nil , err
3659+ }
3660+
3661+ items = append (items , objKeyLiveTarget {kube .NewResourceKey (res .Group , res .Kind , res .Namespace , res .Name ), live , target })
3662+ }
3663+ }
3664+
3665+ return items , nil
3666+ }
0 commit comments