Skip to content

Commit ad0f352

Browse files
authored
feat(cli): Support Server-Side Diff CLI (argoproj#23978)
Signed-off-by: Peter Jiang <[email protected]>
1 parent 20c5685 commit ad0f352

File tree

10 files changed

+1812
-253
lines changed

10 files changed

+1812
-253
lines changed

assets/swagger.json

Lines changed: 64 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cmd/argocd/commands/app.go

Lines changed: 189 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -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
14261544
type 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

Comments
 (0)