mirror of
https://github.com/argoproj/argo-cd
synced 2026-04-21 17:07:16 +00:00
feat(cli): Support Server-Side Diff CLI (#23978)
Signed-off-by: Peter Jiang <peterjiang823@gmail.com>
This commit is contained in:
parent
24b0ecc657
commit
6a2077642e
10 changed files with 1791 additions and 232 deletions
64
assets/swagger.json
generated
64
assets/swagger.json
generated
|
|
@ -374,6 +374,56 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/applications/{appName}/server-side-diff": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"ApplicationService"
|
||||
],
|
||||
"summary": "ServerSideDiff performs server-side diff calculation using dry-run apply",
|
||||
"operationId": "ApplicationService_ServerSideDiff",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"name": "appName",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "appNamespace",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "project",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"collectionFormat": "multi",
|
||||
"name": "targetManifests",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "A successful response.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/applicationApplicationServerSideDiffResponse"
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "An unexpected error response.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/runtimeError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/applications/{application.metadata.name}": {
|
||||
"put": {
|
||||
"tags": [
|
||||
|
|
@ -5019,6 +5069,20 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"applicationApplicationServerSideDiffResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/v1alpha1ResourceDiff"
|
||||
}
|
||||
},
|
||||
"modified": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"applicationApplicationSyncRequest": {
|
||||
"type": "object",
|
||||
"title": "ApplicationSyncRequest is a request to apply the config state to live state",
|
||||
|
|
|
|||
|
|
@ -39,9 +39,13 @@ import (
|
|||
"github.com/argoproj/argo-cd/v3/cmd/argocd/commands/headless"
|
||||
"github.com/argoproj/argo-cd/v3/cmd/argocd/commands/utils"
|
||||
cmdutil "github.com/argoproj/argo-cd/v3/cmd/util"
|
||||
argocommon "github.com/argoproj/argo-cd/v3/common"
|
||||
"github.com/argoproj/argo-cd/v3/controller"
|
||||
argocdclient "github.com/argoproj/argo-cd/v3/pkg/apiclient"
|
||||
"github.com/argoproj/argo-cd/v3/pkg/apiclient/application"
|
||||
|
||||
resourceutil "github.com/argoproj/gitops-engine/pkg/sync/resource"
|
||||
|
||||
clusterpkg "github.com/argoproj/argo-cd/v3/pkg/apiclient/cluster"
|
||||
projectpkg "github.com/argoproj/argo-cd/v3/pkg/apiclient/project"
|
||||
"github.com/argoproj/argo-cd/v3/pkg/apiclient/settings"
|
||||
|
|
@ -1282,6 +1286,7 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
|||
revision string
|
||||
localRepoRoot string
|
||||
serverSideGenerate bool
|
||||
serverSideDiff bool
|
||||
localIncludes []string
|
||||
appNamespace string
|
||||
revisions []string
|
||||
|
|
@ -1344,6 +1349,22 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
|||
argoSettings, err := settingsIf.Get(ctx, &settings.SettingsQuery{})
|
||||
errors.CheckError(err)
|
||||
diffOption := &DifferenceOption{}
|
||||
|
||||
hasServerSideDiffAnnotation := resourceutil.HasAnnotationOption(app, argocommon.AnnotationCompareOptions, "ServerSideDiff=true")
|
||||
|
||||
// Use annotation if flag not explicitly set
|
||||
if !c.Flags().Changed("server-side-diff") {
|
||||
serverSideDiff = hasServerSideDiffAnnotation
|
||||
} else if serverSideDiff && !hasServerSideDiffAnnotation {
|
||||
// Flag explicitly set to true, but app annotation is not set
|
||||
fmt.Fprintf(os.Stderr, "Warning: Application does not have ServerSideDiff=true annotation.\n")
|
||||
}
|
||||
|
||||
// Server side diff with local requires server side generate to be set as there will be a mismatch with client-generated manifests.
|
||||
if serverSideDiff && local != "" && !serverSideGenerate {
|
||||
log.Fatal("--server-side-diff with --local requires --server-side-generate.")
|
||||
}
|
||||
|
||||
switch {
|
||||
case app.Spec.HasMultipleSources() && len(revisions) > 0 && len(sourcePositions) > 0:
|
||||
numOfSources := int64(len(app.Spec.GetSources()))
|
||||
|
|
@ -1399,7 +1420,8 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
|||
}
|
||||
}
|
||||
proj := getProject(ctx, c, clientOpts, app.Spec.Project)
|
||||
foundDiffs := findandPrintDiff(ctx, app, proj.Project, resources, argoSettings, diffOption, ignoreNormalizerOpts)
|
||||
|
||||
foundDiffs := findAndPrintDiff(ctx, app, proj.Project, resources, argoSettings, diffOption, ignoreNormalizerOpts, serverSideDiff, appIf, app.GetName(), app.GetNamespace())
|
||||
if foundDiffs && exitCode {
|
||||
os.Exit(diffExitCode)
|
||||
}
|
||||
|
|
@ -1413,6 +1435,7 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
|||
command.Flags().StringVar(&revision, "revision", "", "Compare live app to a particular revision")
|
||||
command.Flags().StringVar(&localRepoRoot, "local-repo-root", "/", "Path to the repository root. Used together with --local allows setting the repository root")
|
||||
command.Flags().BoolVar(&serverSideGenerate, "server-side-generate", false, "Used with --local, this will send your manifests to the server for diffing")
|
||||
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.")
|
||||
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.")
|
||||
command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Only render the difference in namespace")
|
||||
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
|
|||
return command
|
||||
}
|
||||
|
||||
// printResourceDiff prints the diff header and calls cli.PrintDiff for a resource
|
||||
func printResourceDiff(group, kind, namespace, name string, live, target *unstructured.Unstructured) {
|
||||
fmt.Printf("\n===== %s/%s %s/%s ======\n", group, kind, namespace, name)
|
||||
_ = cli.PrintDiff(name, live, target)
|
||||
}
|
||||
|
||||
// findAndPrintServerSideDiff performs a server-side diff by making requests to the api server and prints the response
|
||||
func findAndPrintServerSideDiff(ctx context.Context, app *argoappv1.Application, items []objKeyLiveTarget, resources *application.ManagedResourcesResponse, appIf application.ApplicationServiceClient, appName, appNs string) bool {
|
||||
// Process each item for server-side diff
|
||||
foundDiffs := false
|
||||
for _, item := range items {
|
||||
if item.target != nil && hook.IsHook(item.target) || item.live != nil && hook.IsHook(item.live) {
|
||||
continue
|
||||
}
|
||||
|
||||
// For server-side diff, we need to create aligned arrays for this specific resource
|
||||
var liveResource *argoappv1.ResourceDiff
|
||||
var targetManifest string
|
||||
|
||||
if item.live != nil {
|
||||
for _, res := range resources.Items {
|
||||
if res.Group == item.key.Group && res.Kind == item.key.Kind &&
|
||||
res.Namespace == item.key.Namespace && res.Name == item.key.Name {
|
||||
liveResource = res
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if liveResource == nil {
|
||||
// Create empty live resource for creation case
|
||||
liveResource = &argoappv1.ResourceDiff{
|
||||
Group: item.key.Group,
|
||||
Kind: item.key.Kind,
|
||||
Namespace: item.key.Namespace,
|
||||
Name: item.key.Name,
|
||||
LiveState: "",
|
||||
TargetState: "",
|
||||
Modified: true,
|
||||
}
|
||||
}
|
||||
|
||||
if item.target != nil {
|
||||
jsonBytes, err := json.Marshal(item.target)
|
||||
if err != nil {
|
||||
errors.CheckError(fmt.Errorf("error marshaling target object: %w", err))
|
||||
}
|
||||
targetManifest = string(jsonBytes)
|
||||
}
|
||||
|
||||
// Call server-side diff for this individual resource
|
||||
serverSideDiffQuery := &application.ApplicationServerSideDiffQuery{
|
||||
AppName: &appName,
|
||||
AppNamespace: &appNs,
|
||||
Project: &app.Spec.Project,
|
||||
LiveResources: []*argoappv1.ResourceDiff{liveResource},
|
||||
TargetManifests: []string{targetManifest},
|
||||
}
|
||||
|
||||
serverSideDiffRes, err := appIf.ServerSideDiff(ctx, serverSideDiffQuery)
|
||||
if err != nil {
|
||||
errors.CheckError(err)
|
||||
}
|
||||
|
||||
// Extract diff for this resource
|
||||
for _, resultItem := range serverSideDiffRes.Items {
|
||||
if resultItem.Hook || (!resultItem.Modified && resultItem.TargetState != "" && resultItem.LiveState != "") {
|
||||
continue
|
||||
}
|
||||
|
||||
if resultItem.Modified || resultItem.TargetState == "" || resultItem.LiveState == "" {
|
||||
var live, target *unstructured.Unstructured
|
||||
|
||||
if resultItem.TargetState != "" && resultItem.TargetState != "null" {
|
||||
target = &unstructured.Unstructured{}
|
||||
err = json.Unmarshal([]byte(resultItem.TargetState), target)
|
||||
errors.CheckError(err)
|
||||
}
|
||||
|
||||
if resultItem.LiveState != "" && resultItem.LiveState != "null" {
|
||||
live = &unstructured.Unstructured{}
|
||||
err = json.Unmarshal([]byte(resultItem.LiveState), live)
|
||||
errors.CheckError(err)
|
||||
}
|
||||
|
||||
// Print resulting diff for this resource
|
||||
foundDiffs = true
|
||||
printResourceDiff(resultItem.Group, resultItem.Kind, resultItem.Namespace, resultItem.Name, live, target)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return foundDiffs
|
||||
}
|
||||
|
||||
// DifferenceOption struct to store diff options
|
||||
type DifferenceOption struct {
|
||||
local string
|
||||
|
|
@ -1433,47 +1551,15 @@ type DifferenceOption struct {
|
|||
revisions []string
|
||||
}
|
||||
|
||||
// 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
|
||||
func findandPrintDiff(ctx context.Context, app *argoappv1.Application, proj *argoappv1.AppProject, resources *application.ManagedResourcesResponse, argoSettings *settings.Settings, diffOptions *DifferenceOption, ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts) bool {
|
||||
// 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
|
||||
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 {
|
||||
var foundDiffs bool
|
||||
liveObjs, err := cmdutil.LiveObjects(resources.Items)
|
||||
|
||||
items, err := prepareObjectsForDiff(ctx, app, proj, resources, argoSettings, diffOptions)
|
||||
errors.CheckError(err)
|
||||
items := make([]objKeyLiveTarget, 0)
|
||||
switch {
|
||||
case diffOptions.local != "":
|
||||
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)
|
||||
items = groupObjsForDiff(resources, localObjs, items, argoSettings, app.InstanceName(argoSettings.ControllerNamespace), app.Spec.Destination.Namespace)
|
||||
case diffOptions.revision != "" || len(diffOptions.revisions) > 0:
|
||||
var unstructureds []*unstructured.Unstructured
|
||||
for _, mfst := range diffOptions.res.Manifests {
|
||||
obj, err := argoappv1.UnmarshalToUnstructured(mfst)
|
||||
errors.CheckError(err)
|
||||
unstructureds = append(unstructureds, obj)
|
||||
}
|
||||
groupedObjs := groupObjsByKey(unstructureds, liveObjs, app.Spec.Destination.Namespace)
|
||||
items = groupObjsForDiff(resources, groupedObjs, items, argoSettings, app.InstanceName(argoSettings.ControllerNamespace), app.Spec.Destination.Namespace)
|
||||
case diffOptions.serversideRes != nil:
|
||||
var unstructureds []*unstructured.Unstructured
|
||||
for _, mfst := range diffOptions.serversideRes.Manifests {
|
||||
obj, err := argoappv1.UnmarshalToUnstructured(mfst)
|
||||
errors.CheckError(err)
|
||||
unstructureds = append(unstructureds, obj)
|
||||
}
|
||||
groupedObjs := groupObjsByKey(unstructureds, liveObjs, app.Spec.Destination.Namespace)
|
||||
items = groupObjsForDiff(resources, groupedObjs, items, argoSettings, app.InstanceName(argoSettings.ControllerNamespace), app.Spec.Destination.Namespace)
|
||||
default:
|
||||
for i := range resources.Items {
|
||||
res := resources.Items[i]
|
||||
live := &unstructured.Unstructured{}
|
||||
err := json.Unmarshal([]byte(res.NormalizedLiveState), &live)
|
||||
errors.CheckError(err)
|
||||
|
||||
target := &unstructured.Unstructured{}
|
||||
err = json.Unmarshal([]byte(res.TargetState), &target)
|
||||
errors.CheckError(err)
|
||||
|
||||
items = append(items, objKeyLiveTarget{kube.NewResourceKey(res.Group, res.Kind, res.Namespace, res.Name), live, target})
|
||||
}
|
||||
if useServerSideDiff {
|
||||
return findAndPrintServerSideDiff(ctx, app, items, resources, appIf, appName, appNs)
|
||||
}
|
||||
|
||||
for _, item := range items {
|
||||
|
|
@ -1500,7 +1586,6 @@ func findandPrintDiff(ctx context.Context, app *argoappv1.Application, proj *arg
|
|||
errors.CheckError(err)
|
||||
|
||||
if diffRes.Modified || item.target == nil || item.live == nil {
|
||||
fmt.Printf("\n===== %s/%s %s/%s ======\n", item.key.Group, item.key.Kind, item.key.Namespace, item.key.Name)
|
||||
var live *unstructured.Unstructured
|
||||
var target *unstructured.Unstructured
|
||||
if item.target != nil && item.live != nil {
|
||||
|
|
@ -1512,10 +1597,8 @@ func findandPrintDiff(ctx context.Context, app *argoappv1.Application, proj *arg
|
|||
live = item.live
|
||||
target = item.target
|
||||
}
|
||||
if !foundDiffs {
|
||||
foundDiffs = true
|
||||
}
|
||||
_ = cli.PrintDiff(item.key.Name, live, target)
|
||||
foundDiffs = true
|
||||
printResourceDiff(item.key.Group, item.key.Kind, item.key.Namespace, item.key.Name, live, target)
|
||||
}
|
||||
}
|
||||
return foundDiffs
|
||||
|
|
@ -2297,7 +2380,11 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
|||
fmt.Printf("====== Previewing differences between live and desired state of application %s ======\n", appQualifiedName)
|
||||
|
||||
proj := getProject(ctx, c, clientOpts, app.Spec.Project)
|
||||
foundDiffs = findandPrintDiff(ctx, app, proj.Project, resources, argoSettings, diffOption, ignoreNormalizerOpts)
|
||||
|
||||
// Check if application has ServerSideDiff annotation
|
||||
serverSideDiff := resourceutil.HasAnnotationOption(app, argocommon.AnnotationCompareOptions, "ServerSideDiff=true")
|
||||
|
||||
foundDiffs = findAndPrintDiff(ctx, app, proj.Project, resources, argoSettings, diffOption, ignoreNormalizerOpts, serverSideDiff, appIf, appName, appNs)
|
||||
if !foundDiffs {
|
||||
fmt.Printf("====== No Differences found ======\n")
|
||||
// if no differences found, then no need to sync
|
||||
|
|
@ -3520,3 +3607,60 @@ func NewApplicationConfirmDeletionCommand(clientOpts *argocdclient.ClientOptions
|
|||
command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Namespace of the target application where the source will be appended")
|
||||
return command
|
||||
}
|
||||
|
||||
// prepareObjectsForDiff prepares objects for diffing using the switch statement
|
||||
// to handle different diff options and building the objKeyLiveTarget items
|
||||
func prepareObjectsForDiff(ctx context.Context, app *argoappv1.Application, proj *argoappv1.AppProject, resources *application.ManagedResourcesResponse, argoSettings *settings.Settings, diffOptions *DifferenceOption) ([]objKeyLiveTarget, error) {
|
||||
liveObjs, err := cmdutil.LiveObjects(resources.Items)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]objKeyLiveTarget, 0)
|
||||
|
||||
switch {
|
||||
case diffOptions.local != "":
|
||||
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)
|
||||
items = groupObjsForDiff(resources, localObjs, items, argoSettings, app.InstanceName(argoSettings.ControllerNamespace), app.Spec.Destination.Namespace)
|
||||
case diffOptions.revision != "" || len(diffOptions.revisions) > 0:
|
||||
var unstructureds []*unstructured.Unstructured
|
||||
for _, mfst := range diffOptions.res.Manifests {
|
||||
obj, err := argoappv1.UnmarshalToUnstructured(mfst)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
unstructureds = append(unstructureds, obj)
|
||||
}
|
||||
groupedObjs := groupObjsByKey(unstructureds, liveObjs, app.Spec.Destination.Namespace)
|
||||
items = groupObjsForDiff(resources, groupedObjs, items, argoSettings, app.InstanceName(argoSettings.ControllerNamespace), app.Spec.Destination.Namespace)
|
||||
case diffOptions.serversideRes != nil:
|
||||
var unstructureds []*unstructured.Unstructured
|
||||
for _, mfst := range diffOptions.serversideRes.Manifests {
|
||||
obj, err := argoappv1.UnmarshalToUnstructured(mfst)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
unstructureds = append(unstructureds, obj)
|
||||
}
|
||||
groupedObjs := groupObjsByKey(unstructureds, liveObjs, app.Spec.Destination.Namespace)
|
||||
items = groupObjsForDiff(resources, groupedObjs, items, argoSettings, app.InstanceName(argoSettings.ControllerNamespace), app.Spec.Destination.Namespace)
|
||||
default:
|
||||
for i := range resources.Items {
|
||||
res := resources.Items[i]
|
||||
live := &unstructured.Unstructured{}
|
||||
err := json.Unmarshal([]byte(res.NormalizedLiveState), &live)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
target := &unstructured.Unstructured{}
|
||||
err = json.Unmarshal([]byte(res.TargetState), &target)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
items = append(items, objKeyLiveTarget{kube.NewResourceKey(res.Group, res.Kind, res.Namespace, res.Name), live, target})
|
||||
}
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2253,6 +2253,10 @@ func (c *fakeAppServiceClient) ListResourceLinks(_ context.Context, _ *applicati
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
func (c *fakeAppServiceClient) ServerSideDiff(_ context.Context, _ *applicationpkg.ApplicationServerSideDiffQuery, _ ...grpc.CallOption) (*applicationpkg.ApplicationServerSideDiffResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type fakeAcdClient struct {
|
||||
simulateTimeout uint
|
||||
}
|
||||
|
|
|
|||
1
docs/user-guide/commands/argocd_app_diff.md
generated
1
docs/user-guide/commands/argocd_app_diff.md
generated
|
|
@ -30,6 +30,7 @@ argocd app diff APPNAME [flags]
|
|||
--refresh Refresh application data when retrieving
|
||||
--revision string Compare live app to a particular revision
|
||||
--revisions stringArray Show manifests at specific revisions for source position in source-positions
|
||||
--server-side-diff Use server-side diff to calculate the diff. This will default to true if the ServerSideDiff annotation is set on the application.
|
||||
--server-side-generate Used with --local, this will send your manifests to the server for diffing
|
||||
--source-names stringArray List of source names. Default is an empty array.
|
||||
--source-positions int64Slice List of source positions. Default is empty array. Counting start at 1. (default [])
|
||||
|
|
|
|||
1066
pkg/apiclient/application/application.pb.go
generated
1066
pkg/apiclient/application/application.pb.go
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1223,6 +1223,78 @@ func local_request_ApplicationService_ManagedResources_0(ctx context.Context, ma
|
|||
|
||||
}
|
||||
|
||||
var (
|
||||
filter_ApplicationService_ServerSideDiff_0 = &utilities.DoubleArray{Encoding: map[string]int{"appName": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}}
|
||||
)
|
||||
|
||||
func request_ApplicationService_ServerSideDiff_0(ctx context.Context, marshaler runtime.Marshaler, client ApplicationServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq ApplicationServerSideDiffQuery
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
var (
|
||||
val string
|
||||
ok bool
|
||||
err error
|
||||
_ = err
|
||||
)
|
||||
|
||||
val, ok = pathParams["appName"]
|
||||
if !ok {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "appName")
|
||||
}
|
||||
|
||||
protoReq.AppName, err = runtime.StringP(val)
|
||||
|
||||
if err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "appName", err)
|
||||
}
|
||||
|
||||
if err := req.ParseForm(); err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_ApplicationService_ServerSideDiff_0); err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
|
||||
msg, err := client.ServerSideDiff(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
func local_request_ApplicationService_ServerSideDiff_0(ctx context.Context, marshaler runtime.Marshaler, server ApplicationServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var protoReq ApplicationServerSideDiffQuery
|
||||
var metadata runtime.ServerMetadata
|
||||
|
||||
var (
|
||||
val string
|
||||
ok bool
|
||||
err error
|
||||
_ = err
|
||||
)
|
||||
|
||||
val, ok = pathParams["appName"]
|
||||
if !ok {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "appName")
|
||||
}
|
||||
|
||||
protoReq.AppName, err = runtime.StringP(val)
|
||||
|
||||
if err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "appName", err)
|
||||
}
|
||||
|
||||
if err := req.ParseForm(); err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_ApplicationService_ServerSideDiff_0); err != nil {
|
||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
||||
}
|
||||
|
||||
msg, err := server.ServerSideDiff(ctx, &protoReq)
|
||||
return msg, metadata, err
|
||||
|
||||
}
|
||||
|
||||
var (
|
||||
filter_ApplicationService_ResourceTree_0 = &utilities.DoubleArray{Encoding: map[string]int{"applicationName": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}}
|
||||
)
|
||||
|
|
@ -2557,6 +2629,29 @@ func RegisterApplicationServiceHandlerServer(ctx context.Context, mux *runtime.S
|
|||
|
||||
})
|
||||
|
||||
mux.Handle("GET", pattern_ApplicationService_ServerSideDiff_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
var stream runtime.ServerTransportStream
|
||||
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := local_request_ApplicationService_ServerSideDiff_0(rctx, inboundMarshaler, server, req, pathParams)
|
||||
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
|
||||
ctx = runtime.NewServerMetadataContext(ctx, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_ApplicationService_ServerSideDiff_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
mux.Handle("GET", pattern_ApplicationService_ResourceTree_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
|
|
@ -3212,6 +3307,26 @@ func RegisterApplicationServiceHandlerClient(ctx context.Context, mux *runtime.S
|
|||
|
||||
})
|
||||
|
||||
mux.Handle("GET", pattern_ApplicationService_ServerSideDiff_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
||||
rctx, err := runtime.AnnotateContext(ctx, mux, req)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_ApplicationService_ServerSideDiff_0(rctx, inboundMarshaler, client, req, pathParams)
|
||||
ctx = runtime.NewServerMetadataContext(ctx, md)
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
|
||||
forward_ApplicationService_ServerSideDiff_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
|
||||
})
|
||||
|
||||
mux.Handle("GET", pattern_ApplicationService_ResourceTree_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
|
|
@ -3530,6 +3645,8 @@ var (
|
|||
|
||||
pattern_ApplicationService_ManagedResources_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3, 2, 4}, []string{"api", "v1", "applications", "applicationName", "managed-resources"}, "", runtime.AssumeColonVerbOpt(true)))
|
||||
|
||||
pattern_ApplicationService_ServerSideDiff_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3, 2, 4}, []string{"api", "v1", "applications", "appName", "server-side-diff"}, "", runtime.AssumeColonVerbOpt(true)))
|
||||
|
||||
pattern_ApplicationService_ResourceTree_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 1, 5, 3, 2, 4}, []string{"api", "v1", "applications", "applicationName", "resource-tree"}, "", runtime.AssumeColonVerbOpt(true)))
|
||||
|
||||
pattern_ApplicationService_WatchResourceTree_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 1, 5, 4, 2, 5}, []string{"api", "v1", "stream", "applications", "applicationName", "resource-tree"}, "", runtime.AssumeColonVerbOpt(true)))
|
||||
|
|
@ -3594,6 +3711,8 @@ var (
|
|||
|
||||
forward_ApplicationService_ManagedResources_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_ApplicationService_ServerSideDiff_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_ApplicationService_ResourceTree_0 = runtime.ForwardResponseMessage
|
||||
|
||||
forward_ApplicationService_WatchResourceTree_0 = runtime.ForwardResponseStream
|
||||
|
|
|
|||
|
|
@ -13,12 +13,11 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/health"
|
||||
|
||||
cacheutil "github.com/argoproj/argo-cd/v3/util/cache"
|
||||
|
||||
kubecache "github.com/argoproj/gitops-engine/pkg/cache"
|
||||
"github.com/argoproj/gitops-engine/pkg/diff"
|
||||
"github.com/argoproj/gitops-engine/pkg/health"
|
||||
"github.com/argoproj/gitops-engine/pkg/sync/common"
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/kube"
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/text"
|
||||
|
|
@ -44,6 +43,7 @@ import (
|
|||
argocommon "github.com/argoproj/argo-cd/v3/common"
|
||||
"github.com/argoproj/argo-cd/v3/pkg/apiclient/application"
|
||||
"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
|
||||
|
||||
appclientset "github.com/argoproj/argo-cd/v3/pkg/client/clientset/versioned"
|
||||
applisters "github.com/argoproj/argo-cd/v3/pkg/client/listers/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/v3/reposerver/apiclient"
|
||||
|
|
@ -63,7 +63,12 @@ import (
|
|||
"github.com/argoproj/argo-cd/v3/util/session"
|
||||
"github.com/argoproj/argo-cd/v3/util/settings"
|
||||
|
||||
resourceutil "github.com/argoproj/gitops-engine/pkg/sync/resource"
|
||||
|
||||
applicationType "github.com/argoproj/argo-cd/v3/pkg/apis/application"
|
||||
argodiff "github.com/argoproj/argo-cd/v3/util/argo/diff"
|
||||
"github.com/argoproj/argo-cd/v3/util/argo/normalizers"
|
||||
kubeutil "github.com/argoproj/argo-cd/v3/util/kube"
|
||||
)
|
||||
|
||||
type AppResourceTreeFn func(ctx context.Context, app *v1alpha1.Application) (*v1alpha1.ApplicationTree, error)
|
||||
|
|
@ -243,6 +248,9 @@ func (s *Server) getAppEnforceRBAC(ctx context.Context, action, project, namespa
|
|||
func (s *Server) getApplicationEnforceRBACInformer(ctx context.Context, action, project, namespace, name string) (*v1alpha1.Application, *v1alpha1.AppProject, error) {
|
||||
namespaceOrDefault := s.appNamespaceOrDefault(namespace)
|
||||
return s.getAppEnforceRBAC(ctx, action, project, namespaceOrDefault, name, func() (*v1alpha1.Application, error) {
|
||||
if !s.isNamespaceEnabled(namespaceOrDefault) {
|
||||
return nil, security.NamespaceNotPermittedError(namespaceOrDefault)
|
||||
}
|
||||
return s.appLister.Applications(namespaceOrDefault).Get(name)
|
||||
})
|
||||
}
|
||||
|
|
@ -2824,3 +2832,171 @@ func getProjectsFromApplicationQuery(q application.ApplicationQuery) []string {
|
|||
}
|
||||
return q.Projects
|
||||
}
|
||||
|
||||
// ServerSideDiff gets the destination cluster and creates a server-side dry run applier and performs the diff
|
||||
// It returns the diff result in the form of a list of ResourceDiffs.
|
||||
func (s *Server) ServerSideDiff(ctx context.Context, q *application.ApplicationServerSideDiffQuery) (*application.ApplicationServerSideDiffResponse, error) {
|
||||
a, _, err := s.getApplicationEnforceRBACInformer(ctx, rbac.ActionGet, q.GetProject(), q.GetAppNamespace(), q.GetAppName())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting application: %w", err)
|
||||
}
|
||||
|
||||
argoSettings, err := s.settingsMgr.GetSettings()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting ArgoCD settings: %w", err)
|
||||
}
|
||||
|
||||
resourceOverrides, err := s.settingsMgr.GetResourceOverrides()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting resource overrides: %w", err)
|
||||
}
|
||||
|
||||
// Convert to map format expected by DiffConfigBuilder
|
||||
overrides := make(map[string]v1alpha1.ResourceOverride)
|
||||
for k, v := range resourceOverrides {
|
||||
overrides[k] = v
|
||||
}
|
||||
|
||||
// Get cluster connection for server-side dry run
|
||||
cluster, err := argo.GetDestinationCluster(ctx, a.Spec.Destination, s.db)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting destination cluster: %w", err)
|
||||
}
|
||||
|
||||
clusterConfig, err := cluster.RawRestConfig()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting cluster raw REST config: %w", err)
|
||||
}
|
||||
|
||||
// Create server-side diff dry run applier
|
||||
openAPISchema, gvkParser, err := s.kubectl.LoadOpenAPISchema(clusterConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get OpenAPI schema: %w", err)
|
||||
}
|
||||
|
||||
applier, cleanup, err := kubeutil.ManageServerSideDiffDryRuns(clusterConfig, openAPISchema, func(_ string) (kube.CleanupFunc, error) {
|
||||
return func() {}, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating server-side dry run applier: %w", err)
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
dryRunner := diff.NewK8sServerSideDryRunner(applier)
|
||||
|
||||
appLabelKey, err := s.settingsMgr.GetAppInstanceLabelKey()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting app instance label key: %w", err)
|
||||
}
|
||||
|
||||
// Build diff config like the CLI does, but with server-side diff enabled
|
||||
ignoreAggregatedRoles := false
|
||||
diffConfig, err := argodiff.NewDiffConfigBuilder().
|
||||
WithDiffSettings(a.Spec.IgnoreDifferences, overrides, ignoreAggregatedRoles, normalizers.IgnoreNormalizerOpts{}).
|
||||
WithTracking(appLabelKey, argoSettings.TrackingMethod).
|
||||
WithNoCache().
|
||||
WithManager(argocommon.ArgoCDSSAManager).
|
||||
WithServerSideDiff(true).
|
||||
WithServerSideDryRunner(dryRunner).
|
||||
WithGVKParser(gvkParser).
|
||||
WithIgnoreMutationWebhook(!resourceutil.HasAnnotationOption(a, argocommon.AnnotationCompareOptions, "IncludeMutationWebhook=true")).
|
||||
Build()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error building diff config: %w", err)
|
||||
}
|
||||
|
||||
// Convert live resources to unstructured objects
|
||||
liveObjs := make([]*unstructured.Unstructured, 0, len(q.GetLiveResources()))
|
||||
for _, liveResource := range q.GetLiveResources() {
|
||||
if liveResource.LiveState != "" && liveResource.LiveState != "null" {
|
||||
liveObj := &unstructured.Unstructured{}
|
||||
err := json.Unmarshal([]byte(liveResource.LiveState), liveObj)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error unmarshaling live state for %s/%s: %w", liveResource.Kind, liveResource.Name, err)
|
||||
}
|
||||
liveObjs = append(liveObjs, liveObj)
|
||||
} else {
|
||||
liveObjs = append(liveObjs, nil)
|
||||
}
|
||||
}
|
||||
|
||||
// Convert target manifests to unstructured objects
|
||||
targetObjs := make([]*unstructured.Unstructured, 0, len(q.GetTargetManifests()))
|
||||
for i, manifestStr := range q.GetTargetManifests() {
|
||||
obj, err := v1alpha1.UnmarshalToUnstructured(manifestStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error unmarshaling target manifest %d: %w", i, err)
|
||||
}
|
||||
targetObjs = append(targetObjs, obj)
|
||||
}
|
||||
|
||||
diffResults, err := argodiff.StateDiffs(liveObjs, targetObjs, diffConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error performing state diffs: %w", err)
|
||||
}
|
||||
|
||||
// Convert StateDiffs results to ResourceDiff format for API response
|
||||
responseDiffs := make([]*v1alpha1.ResourceDiff, 0, len(diffResults.Diffs))
|
||||
modified := false
|
||||
|
||||
for i, diffRes := range diffResults.Diffs {
|
||||
if diffRes.Modified {
|
||||
modified = true
|
||||
}
|
||||
|
||||
// Extract resource metadata for the diff result. Resources should be pre-aligned by the CLI.
|
||||
var group, kind, namespace, name string
|
||||
var hook bool
|
||||
var resourceVersion string
|
||||
|
||||
// Extract resource metadata for the ResourceDiff response. The CLI sends aligned arrays
|
||||
// of live resources and target manifests, but individual resources may only exist in one
|
||||
// array depending on the operation
|
||||
switch {
|
||||
case i < len(q.GetLiveResources()):
|
||||
// A live resource exists at this index
|
||||
lr := q.GetLiveResources()[i]
|
||||
group = lr.Group
|
||||
kind = lr.Kind
|
||||
namespace = lr.Namespace
|
||||
name = lr.Name
|
||||
hook = lr.Hook
|
||||
resourceVersion = lr.ResourceVersion
|
||||
case i < len(targetObjs) && targetObjs[i] != nil:
|
||||
// A target resource exists at this index, but no live resource exists at this index
|
||||
obj := targetObjs[i]
|
||||
group = obj.GroupVersionKind().Group
|
||||
kind = obj.GroupVersionKind().Kind
|
||||
namespace = obj.GetNamespace()
|
||||
name = obj.GetName()
|
||||
hook = false
|
||||
resourceVersion = ""
|
||||
default:
|
||||
return nil, fmt.Errorf("diff result index %d out of bounds: live resources (%d), target objects (%d)",
|
||||
i, len(q.GetLiveResources()), len(targetObjs))
|
||||
}
|
||||
|
||||
// Create ResourceDiff with StateDiffs results
|
||||
// TargetState = PredictedLive (what the target should be after applying)
|
||||
// LiveState = NormalizedLive (current normalized live state)
|
||||
responseDiffs = append(responseDiffs, &v1alpha1.ResourceDiff{
|
||||
Group: group,
|
||||
Kind: kind,
|
||||
Namespace: namespace,
|
||||
Name: name,
|
||||
TargetState: string(diffRes.PredictedLive),
|
||||
LiveState: string(diffRes.NormalizedLive),
|
||||
Diff: "", // Diff string is generated client-side
|
||||
Hook: hook,
|
||||
Modified: diffRes.Modified,
|
||||
ResourceVersion: resourceVersion,
|
||||
})
|
||||
}
|
||||
|
||||
log.Infof("ServerSideDiff completed with %d results, overall modified: %t", len(responseDiffs), modified)
|
||||
|
||||
return &application.ApplicationServerSideDiffResponse{
|
||||
Items: responseDiffs,
|
||||
Modified: &modified,
|
||||
}, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -318,6 +318,19 @@ message ManagedResourcesResponse {
|
|||
repeated github.com.argoproj.argo_cd.v3.pkg.apis.application.v1alpha1.ResourceDiff items = 1;
|
||||
}
|
||||
|
||||
message ApplicationServerSideDiffQuery {
|
||||
required string appName = 1;
|
||||
optional string appNamespace = 2;
|
||||
optional string project = 3;
|
||||
repeated github.com.argoproj.argo_cd.v3.pkg.apis.application.v1alpha1.ResourceDiff liveResources = 4;
|
||||
repeated string targetManifests = 5;
|
||||
}
|
||||
|
||||
message ApplicationServerSideDiffResponse {
|
||||
repeated github.com.argoproj.argo_cd.v3.pkg.apis.application.v1alpha1.ResourceDiff items = 1;
|
||||
required bool modified = 2;
|
||||
}
|
||||
|
||||
message LinkInfo {
|
||||
required string title = 1;
|
||||
required string url = 2;
|
||||
|
|
@ -442,6 +455,11 @@ service ApplicationService {
|
|||
option (google.api.http).get = "/api/v1/applications/{applicationName}/managed-resources";
|
||||
}
|
||||
|
||||
// ServerSideDiff performs server-side diff calculation using dry-run apply
|
||||
rpc ServerSideDiff(ApplicationServerSideDiffQuery) returns (ApplicationServerSideDiffResponse) {
|
||||
option (google.api.http).get = "/api/v1/applications/{appName}/server-side-diff";
|
||||
}
|
||||
|
||||
// ResourceTree returns resource tree
|
||||
rpc ResourceTree(ResourcesQuery) returns (github.com.argoproj.argo_cd.v3.pkg.apis.application.v1alpha1.ApplicationTree) {
|
||||
option (google.api.http).get = "/api/v1/applications/{applicationName}/resource-tree";
|
||||
|
|
|
|||
|
|
@ -3590,3 +3590,145 @@ func Test_DeepCopyInformers(t *testing.T) {
|
|||
assert.NotSame(t, p, &spList[i])
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerSideDiff(t *testing.T) {
|
||||
// Create test projects (avoid "default" which is already created by newTestAppServerWithEnforcerConfigure)
|
||||
testProj := &v1alpha1.AppProject{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "test-project", Namespace: testNamespace},
|
||||
Spec: v1alpha1.AppProjectSpec{
|
||||
SourceRepos: []string{"*"},
|
||||
Destinations: []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "*"}},
|
||||
},
|
||||
}
|
||||
|
||||
forbiddenProj := &v1alpha1.AppProject{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "forbidden-project", Namespace: testNamespace},
|
||||
Spec: v1alpha1.AppProjectSpec{
|
||||
SourceRepos: []string{"*"},
|
||||
Destinations: []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "*"}},
|
||||
},
|
||||
}
|
||||
|
||||
// Create test applications that will exist in the server
|
||||
testApp := newTestApp(func(app *v1alpha1.Application) {
|
||||
app.Name = "test-app"
|
||||
app.Namespace = testNamespace
|
||||
app.Spec.Project = "test-project"
|
||||
})
|
||||
|
||||
forbiddenApp := newTestApp(func(app *v1alpha1.Application) {
|
||||
app.Name = "forbidden-app"
|
||||
app.Namespace = testNamespace
|
||||
app.Spec.Project = "forbidden-project"
|
||||
})
|
||||
|
||||
appServer := newTestAppServer(t, testProj, forbiddenProj, testApp, forbiddenApp)
|
||||
|
||||
t.Run("InputValidation", func(t *testing.T) {
|
||||
// Test missing application name
|
||||
query := &application.ApplicationServerSideDiffQuery{
|
||||
AppName: ptr.To(""), // Empty name instead of nil
|
||||
AppNamespace: ptr.To(testNamespace),
|
||||
Project: ptr.To("test-project"),
|
||||
LiveResources: []*v1alpha1.ResourceDiff{},
|
||||
TargetManifests: []string{},
|
||||
}
|
||||
|
||||
_, err := appServer.ServerSideDiff(t.Context(), query)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "not found")
|
||||
|
||||
// Test nil application name
|
||||
queryNil := &application.ApplicationServerSideDiffQuery{
|
||||
AppName: nil,
|
||||
AppNamespace: ptr.To(testNamespace),
|
||||
Project: ptr.To("test-project"),
|
||||
LiveResources: []*v1alpha1.ResourceDiff{},
|
||||
TargetManifests: []string{},
|
||||
}
|
||||
|
||||
_, err = appServer.ServerSideDiff(t.Context(), queryNil)
|
||||
assert.Error(t, err)
|
||||
// Should get an error when name is nil
|
||||
})
|
||||
|
||||
t.Run("InvalidManifest", func(t *testing.T) {
|
||||
// Test error handling for malformed JSON in target manifests
|
||||
query := &application.ApplicationServerSideDiffQuery{
|
||||
AppName: ptr.To("test-app"),
|
||||
AppNamespace: ptr.To(testNamespace),
|
||||
Project: ptr.To("test-project"),
|
||||
LiveResources: []*v1alpha1.ResourceDiff{},
|
||||
TargetManifests: []string{`invalid json`},
|
||||
}
|
||||
|
||||
_, err := appServer.ServerSideDiff(t.Context(), query)
|
||||
|
||||
// Should return error for invalid JSON
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "error unmarshaling target manifest")
|
||||
})
|
||||
|
||||
t.Run("InvalidLiveState", func(t *testing.T) {
|
||||
// Test error handling for malformed JSON in live state
|
||||
liveResource := &v1alpha1.ResourceDiff{
|
||||
Group: "apps",
|
||||
Kind: "Deployment",
|
||||
Namespace: "default",
|
||||
Name: "test-deployment",
|
||||
LiveState: `invalid json`,
|
||||
TargetState: "",
|
||||
Modified: true,
|
||||
}
|
||||
|
||||
query := &application.ApplicationServerSideDiffQuery{
|
||||
AppName: ptr.To("test-app"),
|
||||
AppNamespace: ptr.To(testNamespace),
|
||||
Project: ptr.To("test-project"),
|
||||
LiveResources: []*v1alpha1.ResourceDiff{liveResource},
|
||||
TargetManifests: []string{`{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"name":"test"}}`},
|
||||
}
|
||||
|
||||
_, err := appServer.ServerSideDiff(t.Context(), query)
|
||||
|
||||
// Should return error for invalid JSON in live state
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "error unmarshaling live state")
|
||||
})
|
||||
|
||||
t.Run("EmptyRequest", func(t *testing.T) {
|
||||
// Test with empty resources - should succeed without errors but no diffs
|
||||
query := &application.ApplicationServerSideDiffQuery{
|
||||
AppName: ptr.To("test-app"),
|
||||
AppNamespace: ptr.To(testNamespace),
|
||||
Project: ptr.To("test-project"),
|
||||
LiveResources: []*v1alpha1.ResourceDiff{},
|
||||
TargetManifests: []string{},
|
||||
}
|
||||
|
||||
resp, err := appServer.ServerSideDiff(t.Context(), query)
|
||||
|
||||
// Should succeed with empty response
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, resp)
|
||||
assert.False(t, *resp.Modified)
|
||||
assert.Empty(t, resp.Items)
|
||||
})
|
||||
|
||||
t.Run("MissingAppPermission", func(t *testing.T) {
|
||||
// Test RBAC enforcement
|
||||
query := &application.ApplicationServerSideDiffQuery{
|
||||
AppName: ptr.To("nonexistent-app"),
|
||||
AppNamespace: ptr.To(testNamespace),
|
||||
Project: ptr.To("nonexistent-project"),
|
||||
LiveResources: []*v1alpha1.ResourceDiff{},
|
||||
TargetManifests: []string{},
|
||||
}
|
||||
|
||||
_, err := appServer.ServerSideDiff(t.Context(), query)
|
||||
|
||||
// Should fail with permission error since nonexistent-app doesn't exist
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "application")
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3075,3 +3075,198 @@ status:
|
|||
assert.Equal(t, "2023-01-01T00:00:00Z", app.Status.Health.LastTransitionTime.UTC().Format(time.RFC3339))
|
||||
})
|
||||
}
|
||||
|
||||
// TestServerSideDiffCommand tests the --server-side-diff flag for the app diff command
|
||||
func TestServerSideDiffCommand(t *testing.T) {
|
||||
Given(t).
|
||||
Path("two-nice-pods").
|
||||
When().
|
||||
CreateApp().
|
||||
Sync().
|
||||
Then().
|
||||
Expect(OperationPhaseIs(OperationSucceeded)).
|
||||
Expect(SyncStatusIs(SyncStatusCodeSynced)).
|
||||
When().
|
||||
// Create a diff by modifying a pod
|
||||
PatchFile("pod-1.yaml", `[{"op": "add", "path": "/metadata/annotations", "value": {"test": "server-side-diff"}}]`).
|
||||
AddFile("pod-3.yaml", `apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: pod-3
|
||||
annotations:
|
||||
new: "pod"
|
||||
spec:
|
||||
containers:
|
||||
- name: main
|
||||
image: quay.io/argoprojlabs/argocd-e2e-container:0.1
|
||||
imagePullPolicy: IfNotPresent
|
||||
command:
|
||||
- "true"
|
||||
restartPolicy: Never
|
||||
`).
|
||||
Refresh(RefreshTypeHard).
|
||||
Then().
|
||||
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
|
||||
And(func(app *Application) {
|
||||
// Test regular diff command
|
||||
regularOutput, err := fixture.RunCli("app", "diff", app.Name)
|
||||
require.Error(t, err) // diff command returns non-zero exit code when differences found
|
||||
assert.Contains(t, regularOutput, "===== /Pod")
|
||||
assert.Contains(t, regularOutput, "pod-1")
|
||||
assert.Contains(t, regularOutput, "pod-3")
|
||||
|
||||
// Test server-side diff command
|
||||
serverSideOutput, err := fixture.RunCli("app", "diff", app.Name, "--server-side-diff")
|
||||
require.Error(t, err) // diff command returns non-zero exit code when differences found
|
||||
assert.Contains(t, serverSideOutput, "===== /Pod")
|
||||
assert.Contains(t, serverSideOutput, "pod-1")
|
||||
assert.Contains(t, serverSideOutput, "pod-3")
|
||||
|
||||
// Both outputs should contain similar resource headers
|
||||
assert.Contains(t, regularOutput, "test: server-side-diff")
|
||||
assert.Contains(t, serverSideOutput, "test: server-side-diff")
|
||||
assert.Contains(t, regularOutput, "new: pod")
|
||||
assert.Contains(t, serverSideOutput, "new: pod")
|
||||
})
|
||||
}
|
||||
|
||||
// TestServerSideDiffWithSyncedApp tests server-side diff when app is already synced (no differences)
|
||||
func TestServerSideDiffWithSyncedApp(t *testing.T) {
|
||||
Given(t).
|
||||
Path("guestbook").
|
||||
When().
|
||||
CreateApp().
|
||||
Sync().
|
||||
Then().
|
||||
Expect(OperationPhaseIs(OperationSucceeded)).
|
||||
Expect(SyncStatusIs(SyncStatusCodeSynced)).
|
||||
And(func(app *Application) {
|
||||
// Test regular diff command with synced app
|
||||
regularOutput, err := fixture.RunCli("app", "diff", app.Name)
|
||||
require.NoError(t, err) // no differences, should return 0
|
||||
|
||||
// Test server-side diff command with synced app
|
||||
serverSideOutput, err := fixture.RunCli("app", "diff", app.Name, "--server-side-diff")
|
||||
require.NoError(t, err) // no differences, should return 0
|
||||
|
||||
// Both should produce similar output (minimal/no diff output)
|
||||
// The exact output may vary, but both should succeed without errors
|
||||
assert.NotContains(t, regularOutput, "===== ")
|
||||
assert.NotContains(t, serverSideOutput, "===== ")
|
||||
})
|
||||
}
|
||||
|
||||
// TestServerSideDiffWithRevision tests server-side diff with a specific revision
|
||||
func TestServerSideDiffWithRevision(t *testing.T) {
|
||||
Given(t).
|
||||
Path("two-nice-pods").
|
||||
When().
|
||||
CreateApp().
|
||||
Sync().
|
||||
Then().
|
||||
Expect(OperationPhaseIs(OperationSucceeded)).
|
||||
Expect(SyncStatusIs(SyncStatusCodeSynced)).
|
||||
When().
|
||||
PatchFile("pod-1.yaml", `[{"op": "add", "path": "/metadata/labels", "value": {"version": "v1.1"}}]`).
|
||||
Sync().
|
||||
Then().
|
||||
Expect(OperationPhaseIs(OperationSucceeded)).
|
||||
And(func(app *Application) {
|
||||
// Get the current revision
|
||||
currentRevision := ""
|
||||
if len(app.Status.History) > 0 {
|
||||
currentRevision = app.Status.History[len(app.Status.History)-1].Revision
|
||||
}
|
||||
|
||||
if currentRevision != "" {
|
||||
// Test server-side diff with current revision (should show no differences)
|
||||
output, err := fixture.RunCli("app", "diff", app.Name, "--server-side-diff", "--revision", currentRevision)
|
||||
require.NoError(t, err) // no differences expected
|
||||
assert.NotContains(t, output, "===== ")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestServerSideDiffErrorHandling tests error scenarios for server-side diff
|
||||
func TestServerSideDiffErrorHandling(t *testing.T) {
|
||||
Given(t).
|
||||
Path("two-nice-pods").
|
||||
When().
|
||||
CreateApp().
|
||||
Then().
|
||||
And(func(_ *Application) {
|
||||
// Test server-side diff with non-existent app should fail gracefully
|
||||
_, err := fixture.RunCli("app", "diff", "non-existent-app", "--server-side-diff")
|
||||
require.Error(t, err)
|
||||
// Error occurred as expected - this verifies the command fails gracefully
|
||||
})
|
||||
}
|
||||
|
||||
// TestServerSideDiffWithLocal tests server-side diff with --local flag
|
||||
func TestServerSideDiffWithLocal(t *testing.T) {
|
||||
Given(t).
|
||||
Path("guestbook").
|
||||
When().
|
||||
CreateApp().
|
||||
Sync().
|
||||
Then().
|
||||
Expect(OperationPhaseIs(OperationSucceeded)).
|
||||
Expect(SyncStatusIs(SyncStatusCodeSynced)).
|
||||
And(func(_ *Application) {
|
||||
// Modify the live deployment in the cluster to create differences
|
||||
// Apply patches to the deployment
|
||||
_, err := fixture.KubeClientset.AppsV1().Deployments(fixture.DeploymentNamespace()).Patch(t.Context(),
|
||||
"guestbook-ui", types.JSONPatchType, []byte(`[
|
||||
{"op": "add", "path": "/spec/template/spec/containers/0/env", "value": [{"name": "LOCAL_CHANGE", "value": "true"}]},
|
||||
{"op": "replace", "path": "/spec/replicas", "value": 2}
|
||||
]`), metav1.PatchOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the patch was applied by reading back the deployment
|
||||
modifiedDeployment, err := fixture.KubeClientset.AppsV1().Deployments(fixture.DeploymentNamespace()).Get(t.Context(), "guestbook-ui", metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int32(2), *modifiedDeployment.Spec.Replicas, "Replica count should be updated to 2")
|
||||
assert.Len(t, modifiedDeployment.Spec.Template.Spec.Containers[0].Env, 1, "Should have one environment variable")
|
||||
assert.Equal(t, "LOCAL_CHANGE", modifiedDeployment.Spec.Template.Spec.Containers[0].Env[0].Name)
|
||||
}).
|
||||
When().
|
||||
Refresh(RefreshTypeNormal).
|
||||
Then().
|
||||
Expect(SyncStatusIs(SyncStatusCodeOutOfSync)).
|
||||
And(func(app *Application) {
|
||||
// Test regular diff with --local (add --server-side-generate to avoid deprecation warning)
|
||||
regularOutput, err := fixture.RunCli("app", "diff", app.Name, "--local", "testdata", "--server-side-generate")
|
||||
require.Error(t, err) // diff command returns non-zero exit code when differences found
|
||||
assert.Contains(t, regularOutput, "===== apps/Deployment")
|
||||
assert.Contains(t, regularOutput, "guestbook-ui")
|
||||
assert.Contains(t, regularOutput, "replicas:")
|
||||
|
||||
// Test server-side diff with --local (add --server-side-generate for consistency)
|
||||
serverSideOutput, err := fixture.RunCli("app", "diff", app.Name, "--server-side-diff", "--local", "testdata", "--server-side-generate")
|
||||
require.Error(t, err) // diff command returns non-zero exit code when differences found
|
||||
assert.Contains(t, serverSideOutput, "===== apps/Deployment")
|
||||
assert.Contains(t, serverSideOutput, "guestbook-ui")
|
||||
assert.Contains(t, serverSideOutput, "replicas:")
|
||||
|
||||
// Both outputs should show similar differences
|
||||
assert.Contains(t, regularOutput, "replicas: 2")
|
||||
assert.Contains(t, serverSideOutput, "replicas: 2")
|
||||
})
|
||||
}
|
||||
|
||||
func TestServerSideDiffWithLocalValidation(t *testing.T) {
|
||||
Given(t).
|
||||
Path("guestbook").
|
||||
When().
|
||||
CreateApp().
|
||||
Sync().
|
||||
Then().
|
||||
Expect(OperationPhaseIs(OperationSucceeded)).
|
||||
Expect(SyncStatusIs(SyncStatusCodeSynced)).
|
||||
And(func(app *Application) {
|
||||
// Test that --server-side-diff with --local without --server-side-generate fails with proper error
|
||||
_, err := fixture.RunCli("app", "diff", app.Name, "--server-side-diff", "--local", "testdata")
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "--server-side-diff with --local requires --server-side-generate")
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue