Merge branch 'master' of https://github.com/argoproj/argo-cd into proposal-source-policies-poc

Signed-off-by: Oliver Gondža <ogondza@gmail.com>
This commit is contained in:
Oliver Gondža 2026-04-21 09:59:43 +02:00
commit 9165e775f9
No known key found for this signature in database
GPG key ID: 1915AEB43E1FE7B2
15 changed files with 888 additions and 88 deletions

View file

@ -367,7 +367,7 @@ jobs:
with:
package_json_file: ui/package.json
- name: Setup NodeJS
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
# renovate: datasource=node-version packageName=node versioning=node
node-version: '24.14.1'
@ -516,7 +516,7 @@ jobs:
run: |
echo "GOPATH=$HOME/go" >> $GITHUB_ENV
- name: Setup NodeJS
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
# renovate: datasource=node-version packageName=node versioning=node
node-version: '24.14.1'

View file

@ -51,7 +51,7 @@ jobs:
# has been updated (see it's numeric version in action.yaml)
# and update `renovate-version` parameter accordingly
- name: Self-hosted Renovate
uses: renovatebot/github-action@eb932558ad942cccfd8211cf535f17ff183a9f74 #46.1.9
uses: renovatebot/github-action@83ec54fee49ab67d9cd201084c1ff325b4b462e4 #46.1.10
with:
configurationFile: .github/configs/renovate-config.js
token: '${{ steps.get_token.outputs.token }}'

View file

@ -259,42 +259,15 @@ func (m *appStateManager) GetRepoObjs(ctx context.Context, app *v1alpha1.Applica
appNamespace := app.Spec.Destination.Namespace
apiVersions := argo.APIResourcesToStrings(apiResources, true)
if repo.Depth == 0 && syncedRevision != "" && !source.IsRef() && keyManifestGenerateAnnotationExists && keyManifestGenerateAnnotationVal != "" && (syncedRevision != revision || app.Spec.HasMultipleSources()) {
// Validate the manifest-generate-path annotation to avoid generating manifests if it has not changed.
updateRevisionResult, err := repoClient.UpdateRevisionForPaths(ctx, &apiclient.UpdateRevisionForPathsRequest{
Repo: repo,
Revision: revision,
SyncedRevision: syncedRevision,
NoRevisionCache: noRevisionCache,
Paths: path.GetSourceRefreshPaths(app, source),
AppLabelKey: appLabelKey,
AppName: app.InstanceName(m.namespace),
Namespace: appNamespace,
ApplicationSource: &source,
KubeVersion: serverVersion,
ApiVersions: apiVersions,
TrackingMethod: trackingMethod,
RefSources: refSources,
SyncedRefSources: syncedRefSources,
HasMultipleSources: app.Spec.HasMultipleSources(),
InstallationID: installationID,
})
if err != nil {
return nil, nil, false, fmt.Errorf("failed to compare revisions for source %d of %d: %w", i+1, sourceCount, err)
}
if updateRevisionResult.Changes {
revisionsMayHaveChanges = true
}
// Generate manifests should use same revision as updateRevisionForPaths, because HEAD revision may be different between these two calls
if updateRevisionResult.Revision != "" {
revision = updateRevisionResult.Revision
}
} else if !source.IsRef() {
// revisionsMayHaveChanges is set to true if at least one revision is not possible to be updated
// Evaluate if the revision has changes
resolvedRevision, hasChanges, err := m.evaluateRevisionChanges(ctx, repoClient, app, &source, i, repo, revision, refSources, syncedRefSources, noRevisionCache, appLabelKey, serverVersion, apiVersions, trackingMethod, installationID, keyManifestGenerateAnnotationExists, keyManifestGenerateAnnotationVal)
if err != nil {
return nil, nil, false, fmt.Errorf("failed to evaluate revision changes for source %d of %d: %w", i+1, len(sources), err)
}
if hasChanges {
revisionsMayHaveChanges = true
}
revision = resolvedRevision
repos := permittedHelmRepos
helmRepoCreds := permittedHelmCredentials
@ -372,6 +345,86 @@ func (m *appStateManager) GetRepoObjs(ctx context.Context, app *v1alpha1.Applica
return targetObjs, manifestInfos, revisionsMayHaveChanges, nil
}
// evaluateRevisionChanges determines if a source revision has changes compared to the synced revision.
// Returns the resolved revision, whether changes were detected, and any error.
func (m *appStateManager) evaluateRevisionChanges(
ctx context.Context,
repoClient apiclient.RepoServerServiceClient,
app *v1alpha1.Application,
source *v1alpha1.ApplicationSource,
sourceIndex int,
repo *v1alpha1.Repository,
revision string,
refSources map[string]*v1alpha1.RefTarget,
syncedRefSources v1alpha1.RefTargetRevisionMapping,
noRevisionCache bool,
appLabelKey string,
serverVersion string,
apiVersions []string,
trackingMethod string,
installationID string,
keyManifestGenerateAnnotationExists bool,
keyManifestGenerateAnnotationVal string,
) (string, bool, error) {
// For ref source specifically, we always return false since their change are evaluated as part of the source
// referencing them.
if source.IsRef() {
return revision, false, nil
}
// Determine the synced revision and source type for this specific source
var syncedRevision string
if app.Spec.HasMultipleSources() {
if sourceIndex < len(app.Status.Sync.Revisions) {
syncedRevision = app.Status.Sync.Revisions[sourceIndex]
}
} else {
syncedRevision = app.Status.Sync.Revision
}
// if revisions are the same (and we are not using reference sources), we know there is no changes
if syncedRevision == revision && revision != "" && len(refSources) == 0 {
return revision, false, nil
}
appNamespace := app.Spec.Destination.Namespace
if repo.Depth == 0 && syncedRevision != "" && keyManifestGenerateAnnotationExists && keyManifestGenerateAnnotationVal != "" {
// Validate the manifest-generate-path annotation to avoid generating manifests if it has not changed.
updateRevisionResult, err := repoClient.UpdateRevisionForPaths(ctx, &apiclient.UpdateRevisionForPathsRequest{
Repo: repo,
Revision: revision,
SyncedRevision: syncedRevision,
NoRevisionCache: noRevisionCache,
Paths: path.GetSourceRefreshPaths(app, *source),
AppLabelKey: appLabelKey,
AppName: app.InstanceName(m.namespace),
Namespace: appNamespace,
ApplicationSource: source,
KubeVersion: serverVersion,
ApiVersions: apiVersions,
TrackingMethod: trackingMethod,
RefSources: refSources,
SyncedRefSources: syncedRefSources,
HasMultipleSources: app.Spec.HasMultipleSources(),
InstallationID: installationID,
})
if err != nil {
return "", false, err
}
// Generate manifests should use same revision as updateRevisionForPaths, because HEAD revision may be different between these two calls
if updateRevisionResult.Revision != "" {
revision = updateRevisionResult.Revision
}
return revision, updateRevisionResult.Changes, nil
}
// revisionsMayHaveChanges is set to true if at least one revision is not possible to be updated
return revision, true, nil
}
func unmarshalManifests(manifests []string) ([]*unstructured.Unstructured, error) {
targetObjs := make([]*unstructured.Unstructured, 0)
for _, manifest := range manifests {

View file

@ -1,6 +1,7 @@
package controller
import (
"context"
"encoding/json"
"errors"
"strings"
@ -30,6 +31,7 @@ import (
"github.com/argoproj/argo-cd/v3/controller/testdata"
"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v3/reposerver/apiclient"
"github.com/argoproj/argo-cd/v3/reposerver/apiclient/mocks"
"github.com/argoproj/argo-cd/v3/test"
)
@ -2028,3 +2030,190 @@ func Test_isObjRequiresDeletionConfirmation(t *testing.T) {
})
}
}
func Test_evaluateRevisionChanges(t *testing.T) {
tests := []struct {
name string
source *v1alpha1.ApplicationSource
sourceType v1alpha1.ApplicationSourceType
syncPolicy *v1alpha1.SyncPolicy
revision string
appSyncedRevision string
refSources map[string]*v1alpha1.RefTarget
repoDepth int64
keyManifestGenerateAnnotationExists bool
keyManifestGenerateAnnotationVal string
updateRevisionForPathsResponse *apiclient.UpdateRevisionForPathsResponse
expectedRevision string
expectedHasChanges bool
expectUpdateRevisionForPathsCalled bool
}{
{
name: "Ref source returns early with no changes",
source: &v1alpha1.ApplicationSource{
RepoURL: "https://github.com/example/repo",
Ref: "main",
},
sourceType: v1alpha1.ApplicationSourceTypeHelm,
revision: "abc123",
appSyncedRevision: "def456",
expectedRevision: "abc123",
expectedHasChanges: false,
},
{
name: "Same revision with no ref sources returns early",
source: &v1alpha1.ApplicationSource{
RepoURL: "https://github.com/example/repo",
Path: "manifests",
},
sourceType: v1alpha1.ApplicationSourceTypeKustomize,
revision: "abc123",
appSyncedRevision: "abc123",
refSources: map[string]*v1alpha1.RefTarget{},
expectedRevision: "abc123",
expectedHasChanges: false,
},
{
name: "Same revision with ref sources continues to evaluation",
source: &v1alpha1.ApplicationSource{
RepoURL: "https://github.com/example/repo",
Path: "manifests",
},
sourceType: v1alpha1.ApplicationSourceTypeKustomize,
revision: "abc123",
appSyncedRevision: "abc123",
refSources: map[string]*v1alpha1.RefTarget{
"ref1": {Repo: v1alpha1.Repository{Repo: "https://github.com/example/ref"}},
},
repoDepth: 0,
keyManifestGenerateAnnotationExists: true,
keyManifestGenerateAnnotationVal: ".",
updateRevisionForPathsResponse: &apiclient.UpdateRevisionForPathsResponse{
Revision: "abc123",
Changes: false,
},
expectedRevision: "abc123",
expectedHasChanges: false,
expectUpdateRevisionForPathsCalled: true,
},
{
name: "Shallow clone skips UpdateRevisionForPaths",
source: &v1alpha1.ApplicationSource{
RepoURL: "https://github.com/example/repo",
Path: "manifests",
},
sourceType: v1alpha1.ApplicationSourceTypeKustomize,
syncPolicy: &v1alpha1.SyncPolicy{
Automated: &v1alpha1.SyncPolicyAutomated{},
},
revision: "abc123",
appSyncedRevision: "def456",
repoDepth: 1,
keyManifestGenerateAnnotationExists: true,
keyManifestGenerateAnnotationVal: ".",
expectedRevision: "abc123",
expectedHasChanges: true,
expectUpdateRevisionForPathsCalled: false,
},
{
name: "Missing annotation skips UpdateRevisionForPaths",
source: &v1alpha1.ApplicationSource{
RepoURL: "https://github.com/example/repo",
Path: "manifests",
},
sourceType: v1alpha1.ApplicationSourceTypeKustomize,
syncPolicy: &v1alpha1.SyncPolicy{
Automated: &v1alpha1.SyncPolicyAutomated{},
},
revision: "abc123",
appSyncedRevision: "def456",
repoDepth: 0,
keyManifestGenerateAnnotationExists: false,
keyManifestGenerateAnnotationVal: "",
expectedRevision: "abc123",
expectedHasChanges: true,
expectUpdateRevisionForPathsCalled: false,
},
{
name: "UpdateRevisionForPaths returns updated revision",
source: &v1alpha1.ApplicationSource{
RepoURL: "https://github.com/example/repo",
Path: "manifests",
},
sourceType: v1alpha1.ApplicationSourceTypeKustomize,
syncPolicy: &v1alpha1.SyncPolicy{
Automated: &v1alpha1.SyncPolicyAutomated{},
},
revision: "HEAD",
appSyncedRevision: "def456",
repoDepth: 0,
keyManifestGenerateAnnotationExists: true,
keyManifestGenerateAnnotationVal: ".",
updateRevisionForPathsResponse: &apiclient.UpdateRevisionForPathsResponse{
Revision: "abc123resolved",
Changes: true,
},
expectedRevision: "abc123resolved",
expectedHasChanges: true,
expectUpdateRevisionForPathsCalled: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
app := newFakeApp()
app.Spec.SyncPolicy = tt.syncPolicy
app.Status.Sync.Revision = tt.appSyncedRevision
app.Status.SourceType = tt.sourceType
if tt.keyManifestGenerateAnnotationExists {
app.Annotations = map[string]string{
v1alpha1.AnnotationKeyManifestGeneratePaths: tt.keyManifestGenerateAnnotationVal,
}
}
repo := &v1alpha1.Repository{
Repo: tt.source.RepoURL,
Depth: tt.repoDepth,
}
mockRepoClient := &mocks.RepoServerServiceClient{}
if tt.expectUpdateRevisionForPathsCalled {
mockRepoClient.On("UpdateRevisionForPaths", mock.Anything, mock.Anything).Return(tt.updateRevisionForPathsResponse, nil)
}
mgr := &appStateManager{
namespace: "test-namespace",
}
resolvedRevision, hasChanges, err := mgr.evaluateRevisionChanges(
context.Background(),
mockRepoClient,
app,
tt.source,
0, // sourceIndex
repo,
tt.revision,
tt.refSources,
nil,
false,
"app.kubernetes.io/instance",
"v1.28.0",
[]string{"v1"},
"label",
"test-installation",
tt.keyManifestGenerateAnnotationExists,
tt.keyManifestGenerateAnnotationVal,
)
require.NoError(t, err)
assert.Equal(t, tt.expectedRevision, resolvedRevision)
assert.Equal(t, tt.expectedHasChanges, hasChanges)
if tt.expectUpdateRevisionForPathsCalled {
mockRepoClient.AssertExpectations(t)
} else {
mockRepoClient.AssertNotCalled(t, "UpdateRevisionForPaths")
}
})
}
}

View file

@ -188,6 +188,7 @@ func serverSideDiff(config, live *unstructured.Unstructured, opts ...Option) (*D
Normalize(predictedLive, opts...)
unstructured.RemoveNestedField(predictedLive.Object, "metadata", "managedFields")
unstructured.RemoveNestedField(predictedLive.Object, "metadata", "resourceVersion")
predictedLiveBytes, err := json.Marshal(predictedLive)
if err != nil {
@ -196,6 +197,7 @@ func serverSideDiff(config, live *unstructured.Unstructured, opts ...Option) (*D
Normalize(live, opts...)
unstructured.RemoveNestedField(live.Object, "metadata", "managedFields")
unstructured.RemoveNestedField(live.Object, "metadata", "resourceVersion")
liveBytes, err := json.Marshal(live)
if err != nil {
return nil, fmt.Errorf("error marshaling live resource %s/%s: %w", config.GetKind(), config.GetName(), err)

View file

@ -1165,6 +1165,54 @@ func TestServerSideDiff(t *testing.T) {
assert.Empty(t, liveDeploy.Annotations[AnnotationLastAppliedConfig])
})
t.Run("will not report diff for mismatched resourceVersion", func(t *testing.T) {
// given
t.Parallel()
predictedLiveJSON := `{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": {
"name": "my-deploy",
"namespace": "default",
"resourceVersion": "99999",
"managedFields": [{"manager":"argocd-controller","operation":"Apply","fieldsType":"FieldsV1","fieldsV1":{"f:spec":{"f:replicas":{}}}}]
},
"spec": {"replicas": 1}
}`
liveState := StrToUnstructured(`{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": {
"name": "my-deploy",
"namespace": "default",
"resourceVersion": "12345",
"managedFields": [{"manager":"argocd-controller","operation":"Apply","fieldsType":"FieldsV1","fieldsV1":{"f:spec":{"f:replicas":{}}}}]
},
"spec": {"replicas": 1}
}`)
desiredState := StrToUnstructured(`{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": {
"name": "my-deploy",
"namespace": "default"
},
"spec": {"replicas": 1}
}`)
opts := buildOpts(predictedLiveJSON)
opts = append(opts, WithIgnoreMutationWebhook(false))
// when
result, err := serverSideDiff(desiredState, liveState, opts...)
// then
require.NoError(t, err)
assert.NotNil(t, result)
assert.False(t, result.Modified, "mismatched resourceVersion should not cause a diff")
assert.NotContains(t, string(result.PredictedLive), "resourceVersion")
assert.NotContains(t, string(result.NormalizedLive), "resourceVersion")
})
t.Run("will detect ConfigMap data key removal", func(t *testing.T) {
// given
t.Parallel()

16
go.mod
View file

@ -38,7 +38,7 @@ require (
github.com/go-jose/go-jose/v4 v4.1.4
github.com/go-logr/logr v1.4.3
github.com/go-openapi/loads v0.23.3
github.com/go-openapi/runtime v0.29.3
github.com/go-openapi/runtime v0.29.4
github.com/go-playground/webhooks/v6 v6.4.0
github.com/go-redis/cache/v9 v9.0.0
github.com/gobwas/glob v0.2.3
@ -188,21 +188,21 @@ require (
github.com/go-git/go-billy/v5 v5.6.2 // indirect
github.com/go-jose/go-jose/v3 v3.0.5 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/analysis v0.24.3 // indirect
github.com/go-openapi/analysis v0.25.0 // indirect
github.com/go-openapi/errors v0.22.7 // indirect
github.com/go-openapi/jsonpointer v0.22.5 // indirect
github.com/go-openapi/jsonreference v0.21.5 // indirect
github.com/go-openapi/spec v0.22.4 // indirect
github.com/go-openapi/strfmt v0.26.0 // indirect
github.com/go-openapi/strfmt v0.26.1 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-openapi/swag/conv v0.25.5 // indirect
github.com/go-openapi/swag/fileutils v0.25.5 // indirect
github.com/go-openapi/swag/conv v0.26.0 // indirect
github.com/go-openapi/swag/fileutils v0.26.0 // indirect
github.com/go-openapi/swag/jsonname v0.25.5 // indirect
github.com/go-openapi/swag/jsonutils v0.25.5 // indirect
github.com/go-openapi/swag/jsonutils v0.26.0 // indirect
github.com/go-openapi/swag/loading v0.25.5 // indirect
github.com/go-openapi/swag/mangling v0.25.5 // indirect
github.com/go-openapi/swag/stringutils v0.25.5 // indirect
github.com/go-openapi/swag/typeutils v0.25.5 // indirect
github.com/go-openapi/swag/stringutils v0.26.0 // indirect
github.com/go-openapi/swag/typeutils v0.26.0 // indirect
github.com/go-openapi/swag/yamlutils v0.25.5 // indirect
github.com/go-openapi/validate v0.25.2 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect

44
go.sum
View file

@ -335,8 +335,8 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
github.com/go-openapi/analysis v0.24.3 h1:a1hrvMr8X0Xt69KP5uVTu5jH62DscmDifrLzNglAayk=
github.com/go-openapi/analysis v0.24.3/go.mod h1:Nc+dWJ/FxZbhSow5Yh3ozg5CLJioB+XXT6MdLvJUsUw=
github.com/go-openapi/analysis v0.25.0 h1:EnjAq1yO8wEO9HbPmY8vLPEIkdZuuFhCAKBPvCB7bCs=
github.com/go-openapi/analysis v0.25.0/go.mod h1:5WFTRE43WLkPG9r9OtlMfqkkvUTYLVVCIxLlEpyF8kE=
github.com/go-openapi/errors v0.22.7 h1:JLFBGC0Apwdzw3484MmBqspjPbwa2SHvpDm0u5aGhUA=
github.com/go-openapi/errors v0.22.7/go.mod h1://QW6SD9OsWtH6gHllUCddOXDL0tk0ZGNYHwsw4sW3w=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
@ -349,40 +349,40 @@ github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe
github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw=
github.com/go-openapi/loads v0.23.3 h1:g5Xap1JfwKkUnZdn+S0L3SzBDpcTIYzZ5Qaag0YDkKQ=
github.com/go-openapi/loads v0.23.3/go.mod h1:NOH07zLajXo8y55hom0omlHWDVVvCwBM/S+csCK8LqA=
github.com/go-openapi/runtime v0.29.3 h1:h5twGaEqxtQg40ePiYm9vFFH1q06Czd7Ot6ufdK0w/Y=
github.com/go-openapi/runtime v0.29.3/go.mod h1:8A1W0/L5eyNJvKciqZtvIVQvYO66NlB7INMSZ9bw/oI=
github.com/go-openapi/runtime v0.29.4 h1:k2lDxrGoSAJRdhFG2tONKMpkizY/4X1cciSdtzk4Jjo=
github.com/go-openapi/runtime v0.29.4/go.mod h1:K0k/2raY6oqXJnZAgWJB2i/12QKrhUKpZcH4PfV9P18=
github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ=
github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ=
github.com/go-openapi/strfmt v0.26.0 h1:SDdQLyOEqu8W96rO1FRG1fuCtVyzmukky0zcD6gMGLU=
github.com/go-openapi/strfmt v0.26.0/go.mod h1:Zslk5VZPOISLwmWTMBIS7oiVFem1o1EI6zULY8Uer7Y=
github.com/go-openapi/strfmt v0.26.1 h1:7zGCHji7zSYDC2tCXIusoxYQz/48jAf2q+sF6wXTG+c=
github.com/go-openapi/strfmt v0.26.1/go.mod h1:Zslk5VZPOISLwmWTMBIS7oiVFem1o1EI6zULY8Uer7Y=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
github.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g=
github.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k=
github.com/go-openapi/swag/fileutils v0.25.5 h1:B6JTdOcs2c0dBIs9HnkyTW+5gC+8NIhVBUwERkFhMWk=
github.com/go-openapi/swag/fileutils v0.25.5/go.mod h1:V3cT9UdMQIaH4WiTrUc9EPtVA4txS0TOmRURmhGF4kc=
github.com/go-openapi/swag/conv v0.26.0 h1:5yGGsPYI1ZCva93U0AoKi/iZrNhaJEjr324YVsiD89I=
github.com/go-openapi/swag/conv v0.26.0/go.mod h1:tpAmIL7X58VPnHHiSO4uE3jBeRamGsFsfdDeDtb5ECE=
github.com/go-openapi/swag/fileutils v0.26.0 h1:WJoPRvsA7QRiiWluowkLJa9jaYR7FCuxmDvnCgaRRxU=
github.com/go-openapi/swag/fileutils v0.26.0/go.mod h1:0WDJ7lp67eNjPMO50wAWYlKvhOb6CQ37rzR7wrgI8Tc=
github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo=
github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU=
github.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo=
github.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5 h1:SX6sE4FrGb4sEnnxbFL/25yZBb5Hcg1inLeErd86Y1U=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5/go.mod h1:/2KvOTrKWjVA5Xli3DZWdMCZDzz3uV/T7bXwrKWPquo=
github.com/go-openapi/swag/jsonutils v0.26.0 h1:FawFML2iAXsPqmERscuMPIHmFsoP1tOqWkxBaKNMsnA=
github.com/go-openapi/swag/jsonutils v0.26.0/go.mod h1:2VmA0CJlyFqgawOaPI9psnjFDqzyivIqLYN34t9p91E=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0 h1:apqeINu/ICHouqiRZbyFvuDge5jCmmLTqGQ9V95EaOM=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0/go.mod h1:AyM6QT8uz5IdKxk5akv0y6u4QvcL9GWERt0Jx/F/R8Y=
github.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU=
github.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g=
github.com/go-openapi/swag/mangling v0.25.5 h1:hyrnvbQRS7vKePQPHHDso+k6CGn5ZBs5232UqWZmJZw=
github.com/go-openapi/swag/mangling v0.25.5/go.mod h1:6hadXM/o312N/h98RwByLg088U61TPGiltQn71Iw0NY=
github.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M=
github.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII=
github.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E=
github.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc=
github.com/go-openapi/swag/stringutils v0.26.0 h1:qZQngLxs5s7SLijc3N2ZO+fUq2o8LjuWAASSrJuh+xg=
github.com/go-openapi/swag/stringutils v0.26.0/go.mod h1:sWn5uY+QIIspwPhvgnqJsH8xqFT2ZbYcvbcFanRyhFE=
github.com/go-openapi/swag/typeutils v0.26.0 h1:2kdEwdiNWy+JJdOvu5MA2IIg2SylWAFuuyQIKYybfq4=
github.com/go-openapi/swag/typeutils v0.26.0/go.mod h1:oovDuIUvTrEHVMqWilQzKzV4YlSKgyZmFh7AlfABNVE=
github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ=
github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ=
github.com/go-openapi/testify/enable/yaml/v2 v2.4.1 h1:NZOrZmIb6PTv5LTFxr5/mKV/FjbUzGE7E6gLz7vFoOQ=
github.com/go-openapi/testify/enable/yaml/v2 v2.4.1/go.mod h1:r7dwsujEHawapMsxA69i+XMGZrQ5tRauhLAjV/sxg3Q=
github.com/go-openapi/testify/v2 v2.4.1 h1:zB34HDKj4tHwyUQHrUkpV0Q0iXQ6dUCOQtIqn8hE6Iw=
github.com/go-openapi/testify/v2 v2.4.1/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-openapi/testify/enable/yaml/v2 v2.4.2 h1:5zRca5jw7lzVREKCZVNBpysDNBjj74rBh0N2BGQbSR0=
github.com/go-openapi/testify/enable/yaml/v2 v2.4.2/go.mod h1:XVevPw5hUXuV+5AkI1u1PeAm27EQVrhXTTCPAF85LmE=
github.com/go-openapi/testify/v2 v2.4.2 h1:tiByHpvE9uHrrKjOszax7ZvKB7QOgizBWGBLuq0ePx4=
github.com/go-openapi/testify/v2 v2.4.2/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw=
github.com/go-openapi/validate v0.25.2 h1:12NsfLAwGegqbGWr2CnvT65X/Q2USJipmJ9b7xDJZz0=
github.com/go-openapi/validate v0.25.2/go.mod h1:Pgl1LpPPGFnZ+ys4/hTlDiRYQdI1ocKypgE+8Q8BLfY=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=

View file

@ -1,6 +1,7 @@
import globals from 'globals';
import pluginJs from '@eslint/js';
import tseslint from 'typescript-eslint';
import reactX from 'eslint-plugin-react-x';
import pluginReactConfig from 'eslint-plugin-react/configs/recommended.js';
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
@ -15,6 +16,13 @@ export default [
'@typescript-eslint/no-var-requires': 'off'
}
},
{
...reactX.configs.recommended,
rules: {
// ...reactX.configs.strict.rules,
'react-x/no-class-component': 'error'
}
},
{
settings: {
react: {
@ -23,8 +31,11 @@ export default [
},
...pluginReactConfig,
rules: {
// TODO: Re-enable these rules that were disabled by mistake
// ...pluginReactConfig.rules,
'react/display-name': 'off',
'react/no-string-refs': 'off',
'react/prefer-stateless-function': 'error',
'react/jsx-no-useless-fragment': ['error', {allowExpressions: true}]
}
},

View file

@ -108,6 +108,7 @@
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-react": "^7.37.2",
"eslint-plugin-react-x": "^4.2.3",
"globals": "^15.1.0",
"html-webpack-plugin": "^5.6.0",
"identity-obj-proxy": "^3.0.0",

View file

@ -271,6 +271,9 @@ importers:
eslint-plugin-react:
specifier: ^7.37.2
version: 7.37.5(eslint@9.31.0)
eslint-plugin-react-x:
specifier: ^4.2.3
version: 4.2.3(eslint@9.31.0)(typescript@4.9.5)
globals:
specifier: ^15.1.0
version: 15.15.0
@ -1293,10 +1296,51 @@ packages:
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
'@eslint-community/eslint-utils@4.9.1':
resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
'@eslint-community/regexpp@4.12.1':
resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==}
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
'@eslint-react/ast@4.2.3':
resolution: {integrity: sha512-/XHJPFX8lsp+c/gMzFOnIxqH7YIXVX8SlMHuZ6XTUlYHkGquhydTtgso0VFiLQN1z3dThrybdgBq+JD+LSwK2w==}
engines: {node: '>=22.0.0'}
peerDependencies:
eslint: ^10.0.0
typescript: '*'
'@eslint-react/core@4.2.3':
resolution: {integrity: sha512-r0cgJlCemBb61f0qCrXS95hNq2ajIku5V7Tk45fROQu4HIV55ILJeN2ceea1LKmgRWy/pQw8+SvImronwWo16A==}
engines: {node: '>=22.0.0'}
peerDependencies:
eslint: ^10.0.0
typescript: '*'
'@eslint-react/jsx@4.2.3':
resolution: {integrity: sha512-lSwRo/PAwf1EvXRxpXA5yBhPIxahFuC4uHh84nc5OxE0mJ7YEmzmASR+ug3QOnVnfDsJDVo6AWVR7PSL99YkOQ==}
engines: {node: '>=22.0.0'}
peerDependencies:
eslint: ^10.0.0
typescript: '*'
'@eslint-react/shared@4.2.3':
resolution: {integrity: sha512-6HermdKaTWkID0coAK46ynA9XIwUWGgA2Y+NK6qcmL/qbYzyRYs4hq+SmLMvZZ8DV/SFOaHRXl9iCTvjf6DvXQ==}
engines: {node: '>=22.0.0'}
peerDependencies:
eslint: ^10.0.0
typescript: '*'
'@eslint-react/var@4.2.3':
resolution: {integrity: sha512-zkQki2eYbQrMW4O6DCZDQzslFvw0sWAlvW/WWjocEIGHqRGC3IHWcRt3xsq8JPNOW4WjF4/LZ8czkyLoINV9rw==}
engines: {node: '>=22.0.0'}
peerDependencies:
eslint: ^10.0.0
typescript: '*'
'@eslint/config-array@0.21.0':
resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@ -1850,10 +1894,26 @@ packages:
typescript:
optional: true
'@typescript-eslint/project-service@8.58.2':
resolution: {integrity: sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/scope-manager@7.18.0':
resolution: {integrity: sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==}
engines: {node: ^18.18.0 || >=20.0.0}
'@typescript-eslint/scope-manager@8.58.2':
resolution: {integrity: sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/tsconfig-utils@8.58.2':
resolution: {integrity: sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/type-utils@7.18.0':
resolution: {integrity: sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==}
engines: {node: ^18.18.0 || >=20.0.0}
@ -1864,10 +1924,21 @@ packages:
typescript:
optional: true
'@typescript-eslint/type-utils@8.58.2':
resolution: {integrity: sha512-Z7EloNR/B389FvabdGeTo2XMs4W9TjtPiO9DAsmT0yom0bwlPyRjkJ1uCdW1DvrrrYP50AJZ9Xc3sByZA9+dcg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/types@7.18.0':
resolution: {integrity: sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==}
engines: {node: ^18.18.0 || >=20.0.0}
'@typescript-eslint/types@8.58.2':
resolution: {integrity: sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/typescript-estree@7.18.0':
resolution: {integrity: sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==}
engines: {node: ^18.18.0 || >=20.0.0}
@ -1877,16 +1948,33 @@ packages:
typescript:
optional: true
'@typescript-eslint/typescript-estree@8.58.2':
resolution: {integrity: sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/utils@7.18.0':
resolution: {integrity: sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==}
engines: {node: ^18.18.0 || >=20.0.0}
peerDependencies:
eslint: ^8.56.0
'@typescript-eslint/utils@8.58.2':
resolution: {integrity: sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
'@typescript-eslint/visitor-keys@7.18.0':
resolution: {integrity: sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==}
engines: {node: ^18.18.0 || >=20.0.0}
'@typescript-eslint/visitor-keys@8.58.2':
resolution: {integrity: sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@webassemblyjs/ast@1.14.1':
resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==}
@ -2254,6 +2342,10 @@ packages:
resolution: {integrity: sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==}
engines: {node: 18 || 20 || >=22}
brace-expansion@5.0.5:
resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==}
engines: {node: 18 || 20 || >=22}
braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
@ -2450,6 +2542,9 @@ packages:
commondir@1.0.1:
resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==}
compare-versions@6.1.1:
resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==}
component-emitter@1.3.1:
resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==}
@ -2587,6 +2682,15 @@ packages:
supports-color:
optional: true
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
decimal.js@10.6.0:
resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
@ -2889,6 +2993,13 @@ packages:
eslint-config-prettier:
optional: true
eslint-plugin-react-x@4.2.3:
resolution: {integrity: sha512-kJZXa5QsGA4FzuTyKLKjFt9nm78CZcfHshfgfSXjVOshvlVGeg1RWyNZnXDW3hASdZ/REsPg2mGFYqwUPXnJ5Q==}
engines: {node: '>=22.0.0'}
peerDependencies:
eslint: ^10.0.0
typescript: '*'
eslint-plugin-react@7.37.5:
resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==}
engines: {node: '>=4'}
@ -2911,6 +3022,10 @@ packages:
resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
eslint-visitor-keys@5.0.1:
resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
eslint@9.31.0:
resolution: {integrity: sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@ -3023,6 +3138,15 @@ packages:
fb-watchman@2.0.2:
resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==}
fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'}
peerDependencies:
picomatch: ^3 || ^4
peerDependenciesMeta:
picomatch:
optional: true
file-entry-cache@8.0.0:
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
engines: {node: '>=16.0.0'}
@ -4001,6 +4125,10 @@ packages:
minimalistic-assert@1.0.1:
resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==}
minimatch@10.2.5:
resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
engines: {node: 18 || 20 || >=22}
minimatch@3.1.5:
resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==}
@ -4348,6 +4476,10 @@ packages:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'}
picomatch@4.0.4:
resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==}
engines: {node: '>=12'}
pirates@4.0.7:
resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
engines: {node: '>= 6'}
@ -4838,6 +4970,11 @@ packages:
engines: {node: '>=10'}
hasBin: true
semver@7.7.4:
resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==}
engines: {node: '>=10'}
hasBin: true
send@0.19.0:
resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==}
engines: {node: '>= 0.8.0'}
@ -5018,6 +5155,9 @@ packages:
resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==}
engines: {node: '>=10'}
string-ts@2.3.1:
resolution: {integrity: sha512-xSJq+BS52SaFFAVxuStmx6n5aYZU571uYUnUrPXkPFCfdHyZMMlbP2v2Wx5sNBnAVzq/2+0+mcBLBa3Xa5ubYw==}
string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
@ -5179,6 +5319,10 @@ packages:
tiny-warning@1.0.3:
resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==}
tinyglobby@0.2.16:
resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==}
engines: {node: '>=12.0.0'}
tippy.js@5.2.1:
resolution: {integrity: sha512-66UT6JRVn3dXNCORE+0UvUK3JZqV/VhLlU6HTDm3FmrweUUFUxUGvT8tUQ7ycMp+uhuLAwQw6dBabyC+iKf/MA==}
@ -5210,6 +5354,12 @@ packages:
peerDependencies:
typescript: '>=4.2.0'
ts-api-utils@2.5.0:
resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==}
engines: {node: '>=18.12'}
peerDependencies:
typescript: '>=4.8.4'
ts-jest@29.4.0:
resolution: {integrity: sha512-d423TJMnJGu80/eSgfQ5w/R+0zFJvdtTxwtF9KzFFunOpSeD+79lHJQIiAhluJoyGRbvj9NZJsl9WjCUo0ND7Q==}
engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0}
@ -5251,6 +5401,9 @@ packages:
'@swc/wasm':
optional: true
ts-pattern@5.9.0:
resolution: {integrity: sha512-6s5V71mX8qBUmlgbrfL33xDUwO0fq48rxAu2LBE11WBeGdpCPOsXksQbZJHvHwhrd3QjUusd3mAOM5Gg0mFBLg==}
tslib@1.14.1:
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
@ -5651,6 +5804,9 @@ packages:
zod@3.25.76:
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
zod@4.3.6:
resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
snapshots:
'@ampproject/remapping@2.3.0':
@ -6633,8 +6789,75 @@ snapshots:
eslint: 9.31.0
eslint-visitor-keys: 3.4.3
'@eslint-community/eslint-utils@4.9.1(eslint@9.31.0)':
dependencies:
eslint: 9.31.0
eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.12.1': {}
'@eslint-react/ast@4.2.3(eslint@9.31.0)(typescript@4.9.5)':
dependencies:
'@typescript-eslint/types': 8.58.2
'@typescript-eslint/typescript-estree': 8.58.2(typescript@4.9.5)
'@typescript-eslint/utils': 8.58.2(eslint@9.31.0)(typescript@4.9.5)
eslint: 9.31.0
string-ts: 2.3.1
typescript: 4.9.5
transitivePeerDependencies:
- supports-color
'@eslint-react/core@4.2.3(eslint@9.31.0)(typescript@4.9.5)':
dependencies:
'@eslint-react/ast': 4.2.3(eslint@9.31.0)(typescript@4.9.5)
'@eslint-react/jsx': 4.2.3(eslint@9.31.0)(typescript@4.9.5)
'@eslint-react/shared': 4.2.3(eslint@9.31.0)(typescript@4.9.5)
'@eslint-react/var': 4.2.3(eslint@9.31.0)(typescript@4.9.5)
'@typescript-eslint/scope-manager': 8.58.2
'@typescript-eslint/types': 8.58.2
'@typescript-eslint/utils': 8.58.2(eslint@9.31.0)(typescript@4.9.5)
eslint: 9.31.0
ts-pattern: 5.9.0
typescript: 4.9.5
transitivePeerDependencies:
- supports-color
'@eslint-react/jsx@4.2.3(eslint@9.31.0)(typescript@4.9.5)':
dependencies:
'@eslint-react/ast': 4.2.3(eslint@9.31.0)(typescript@4.9.5)
'@eslint-react/shared': 4.2.3(eslint@9.31.0)(typescript@4.9.5)
'@eslint-react/var': 4.2.3(eslint@9.31.0)(typescript@4.9.5)
'@typescript-eslint/types': 8.58.2
'@typescript-eslint/utils': 8.58.2(eslint@9.31.0)(typescript@4.9.5)
eslint: 9.31.0
ts-pattern: 5.9.0
typescript: 4.9.5
transitivePeerDependencies:
- supports-color
'@eslint-react/shared@4.2.3(eslint@9.31.0)(typescript@4.9.5)':
dependencies:
'@typescript-eslint/utils': 8.58.2(eslint@9.31.0)(typescript@4.9.5)
eslint: 9.31.0
ts-pattern: 5.9.0
typescript: 4.9.5
zod: 4.3.6
transitivePeerDependencies:
- supports-color
'@eslint-react/var@4.2.3(eslint@9.31.0)(typescript@4.9.5)':
dependencies:
'@eslint-react/ast': 4.2.3(eslint@9.31.0)(typescript@4.9.5)
'@eslint-react/shared': 4.2.3(eslint@9.31.0)(typescript@4.9.5)
'@typescript-eslint/scope-manager': 8.58.2
'@typescript-eslint/types': 8.58.2
'@typescript-eslint/utils': 8.58.2(eslint@9.31.0)(typescript@4.9.5)
eslint: 9.31.0
ts-pattern: 5.9.0
typescript: 4.9.5
transitivePeerDependencies:
- supports-color
'@eslint/config-array@0.21.0':
dependencies:
'@eslint/object-schema': 2.1.6
@ -7332,11 +7555,29 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/project-service@8.58.2(typescript@4.9.5)':
dependencies:
'@typescript-eslint/tsconfig-utils': 8.58.2(typescript@4.9.5)
'@typescript-eslint/types': 8.58.2
debug: 4.4.3
typescript: 4.9.5
transitivePeerDependencies:
- supports-color
'@typescript-eslint/scope-manager@7.18.0':
dependencies:
'@typescript-eslint/types': 7.18.0
'@typescript-eslint/visitor-keys': 7.18.0
'@typescript-eslint/scope-manager@8.58.2':
dependencies:
'@typescript-eslint/types': 8.58.2
'@typescript-eslint/visitor-keys': 8.58.2
'@typescript-eslint/tsconfig-utils@8.58.2(typescript@4.9.5)':
dependencies:
typescript: 4.9.5
'@typescript-eslint/type-utils@7.18.0(eslint@9.31.0)(typescript@4.9.5)':
dependencies:
'@typescript-eslint/typescript-estree': 7.18.0(typescript@4.9.5)
@ -7349,8 +7590,22 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/type-utils@8.58.2(eslint@9.31.0)(typescript@4.9.5)':
dependencies:
'@typescript-eslint/types': 8.58.2
'@typescript-eslint/typescript-estree': 8.58.2(typescript@4.9.5)
'@typescript-eslint/utils': 8.58.2(eslint@9.31.0)(typescript@4.9.5)
debug: 4.4.3
eslint: 9.31.0
ts-api-utils: 2.5.0(typescript@4.9.5)
typescript: 4.9.5
transitivePeerDependencies:
- supports-color
'@typescript-eslint/types@7.18.0': {}
'@typescript-eslint/types@8.58.2': {}
'@typescript-eslint/typescript-estree@7.18.0(typescript@4.9.5)':
dependencies:
'@typescript-eslint/types': 7.18.0
@ -7366,6 +7621,21 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/typescript-estree@8.58.2(typescript@4.9.5)':
dependencies:
'@typescript-eslint/project-service': 8.58.2(typescript@4.9.5)
'@typescript-eslint/tsconfig-utils': 8.58.2(typescript@4.9.5)
'@typescript-eslint/types': 8.58.2
'@typescript-eslint/visitor-keys': 8.58.2
debug: 4.4.3
minimatch: 10.2.5
semver: 7.7.4
tinyglobby: 0.2.16
ts-api-utils: 2.5.0(typescript@4.9.5)
typescript: 4.9.5
transitivePeerDependencies:
- supports-color
'@typescript-eslint/utils@7.18.0(eslint@9.31.0)(typescript@4.9.5)':
dependencies:
'@eslint-community/eslint-utils': 4.7.0(eslint@9.31.0)
@ -7377,11 +7647,27 @@ snapshots:
- supports-color
- typescript
'@typescript-eslint/utils@8.58.2(eslint@9.31.0)(typescript@4.9.5)':
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@9.31.0)
'@typescript-eslint/scope-manager': 8.58.2
'@typescript-eslint/types': 8.58.2
'@typescript-eslint/typescript-estree': 8.58.2(typescript@4.9.5)
eslint: 9.31.0
typescript: 4.9.5
transitivePeerDependencies:
- supports-color
'@typescript-eslint/visitor-keys@7.18.0':
dependencies:
'@typescript-eslint/types': 7.18.0
eslint-visitor-keys: 3.4.3
'@typescript-eslint/visitor-keys@8.58.2':
dependencies:
'@typescript-eslint/types': 8.58.2
eslint-visitor-keys: 5.0.1
'@webassemblyjs/ast@1.14.1':
dependencies:
'@webassemblyjs/helper-numbers': 1.13.2
@ -7850,6 +8136,10 @@ snapshots:
dependencies:
balanced-match: 4.0.4
brace-expansion@5.0.5:
dependencies:
balanced-match: 4.0.4
braces@3.0.3:
dependencies:
fill-range: 7.1.1
@ -8061,6 +8351,8 @@ snapshots:
commondir@1.0.1: {}
compare-versions@6.1.1: {}
component-emitter@1.3.1: {}
compressible@2.0.18:
@ -8216,6 +8508,10 @@ snapshots:
dependencies:
ms: 2.1.3
debug@4.4.3:
dependencies:
ms: 2.1.3
decimal.js@10.6.0: {}
decko@1.2.0: {}
@ -8587,6 +8883,26 @@ snapshots:
'@types/eslint': 9.6.1
eslint-config-prettier: 9.1.2(eslint@9.31.0)
eslint-plugin-react-x@4.2.3(eslint@9.31.0)(typescript@4.9.5):
dependencies:
'@eslint-react/ast': 4.2.3(eslint@9.31.0)(typescript@4.9.5)
'@eslint-react/core': 4.2.3(eslint@9.31.0)(typescript@4.9.5)
'@eslint-react/jsx': 4.2.3(eslint@9.31.0)(typescript@4.9.5)
'@eslint-react/shared': 4.2.3(eslint@9.31.0)(typescript@4.9.5)
'@eslint-react/var': 4.2.3(eslint@9.31.0)(typescript@4.9.5)
'@typescript-eslint/scope-manager': 8.58.2
'@typescript-eslint/type-utils': 8.58.2(eslint@9.31.0)(typescript@4.9.5)
'@typescript-eslint/types': 8.58.2
'@typescript-eslint/utils': 8.58.2(eslint@9.31.0)(typescript@4.9.5)
compare-versions: 6.1.1
eslint: 9.31.0
string-ts: 2.3.1
ts-api-utils: 2.5.0(typescript@4.9.5)
ts-pattern: 5.9.0
typescript: 4.9.5
transitivePeerDependencies:
- supports-color
eslint-plugin-react@7.37.5(eslint@9.31.0):
dependencies:
array-includes: 3.1.9
@ -8623,6 +8939,8 @@ snapshots:
eslint-visitor-keys@4.2.1: {}
eslint-visitor-keys@5.0.1: {}
eslint@9.31.0:
dependencies:
'@eslint-community/eslint-utils': 4.7.0(eslint@9.31.0)
@ -8793,6 +9111,10 @@ snapshots:
dependencies:
bser: 2.1.1
fdir@6.5.0(picomatch@4.0.4):
optionalDependencies:
picomatch: 4.0.4
file-entry-cache@8.0.0:
dependencies:
flat-cache: 4.0.1
@ -9991,6 +10313,10 @@ snapshots:
minimalistic-assert@1.0.1: {}
minimatch@10.2.5:
dependencies:
brace-expansion: 5.0.5
minimatch@3.1.5:
dependencies:
brace-expansion: 1.1.12
@ -10331,6 +10657,8 @@ snapshots:
picomatch@2.3.1: {}
picomatch@4.0.4: {}
pirates@4.0.7: {}
pkg-dir@4.2.0:
@ -10892,6 +11220,8 @@ snapshots:
semver@7.7.2: {}
semver@7.7.4: {}
send@0.19.0:
dependencies:
debug: 2.6.9
@ -11129,6 +11459,8 @@ snapshots:
char-regex: 1.0.2
strip-ansi: 6.0.1
string-ts@2.3.1: {}
string-width@4.2.3:
dependencies:
emoji-regex: 8.0.0
@ -11333,6 +11665,11 @@ snapshots:
tiny-warning@1.0.3: {}
tinyglobby@0.2.16:
dependencies:
fdir: 6.5.0(picomatch@4.0.4)
picomatch: 4.0.4
tippy.js@5.2.1:
dependencies:
popper.js: 1.16.1
@ -11362,6 +11699,10 @@ snapshots:
dependencies:
typescript: 4.9.5
ts-api-utils@2.5.0(typescript@4.9.5):
dependencies:
typescript: 4.9.5
ts-jest@29.4.0(@babel/core@7.28.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.0))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.14.12)(ts-node@10.9.2(@types/node@20.14.12)(typescript@4.9.5)))(typescript@4.9.5):
dependencies:
bs-logger: 0.2.6
@ -11400,6 +11741,8 @@ snapshots:
v8-compile-cache-lib: 3.0.1
yn: 3.1.1
ts-pattern@5.9.0: {}
tslib@1.14.1: {}
tslib@2.6.2: {}
@ -11851,3 +12194,5 @@ snapshots:
yocto-queue@0.1.0: {}
zod@3.25.76: {}
zod@4.3.6: {}

View file

@ -1295,6 +1295,41 @@ Are you sure you want to disable auto-sync and rollback application '${props.mat
action: () => AppUtils.showDeploy('all', null, appContext),
disabled: !app.spec.source && (!app.spec.sources || app.spec.sources.length === 0) && !app.spec.sourceHydrator
},
{
iconClassName: 'fa fa-toggle-on',
title: <ActionMenuItem actionLabel='Toggle Auto-Sync' />,
action: async () => {
const isEnabled = app.spec.syncPolicy?.automated && app.spec.syncPolicy.automated.enabled !== false;
const confirmationTitle = isEnabled ? 'Disable Auto-Sync?' : 'Enable Auto-Sync?';
const confirmed = await appContext.popup.confirm(
confirmationTitle,
isEnabled
? 'Are you sure you want to disable automated application synchronization'
: 'If enabled, application will automatically sync when changes are detected'
);
if (!confirmed) return;
try {
await services.applications.runResourceAction(
app.metadata.name,
app.metadata.namespace,
{
name: app.metadata.name,
namespace: app.metadata.namespace,
group: 'argoproj.io',
kind: 'Application',
version: 'v1alpha1'
} as appModels.ResourceNode,
'toggle-auto-sync',
[]
);
} catch (e) {
appContext.notifications.show({
content: <ErrorNotification title={`Unable to "${confirmationTitle.replace(/\?/g, '')}"`} e={e} />,
type: NotificationType.Error
});
}
}
},
...(app.status?.operationState?.phase === 'Running' && app.status.resources.find(r => r.requiresDeletionConfirmation)
? [
{

View file

@ -23,7 +23,7 @@ import {services} from '../../../shared/services';
import {ApplicationSyncOptionsField} from '../application-sync-options/application-sync-options';
import {RevisionFormField} from '../revision-form-field/revision-form-field';
import {ComparisonStatusIcon, HealthStatusIcon, syncStatusMessage, urlPattern, formatCreationTimestamp, getAppDefaultSource, getAppSpecDefaultSource} from '../utils';
import {ComparisonStatusIcon, HealthStatusIcon, syncStatusMessage, urlPattern, formatCreationTimestamp, getAppDefaultSource, getAppSpecDefaultSource, appRBACName} from '../utils';
import {ApplicationRetryOptions} from '../application-retry-options/application-retry-options';
import {ApplicationRetryView} from '../application-retry-view/application-retry-view';
import {Link} from 'react-router-dom';
@ -528,6 +528,56 @@ export const ApplicationSummary = (props: ApplicationSummaryProps) => {
]
: [...standardSourceItems, ...sourceItems];
async function toggleAutoSync(ctx: ContextApis) {
const isEnabled = app.spec.syncPolicy?.automated && app.spec.syncPolicy.automated.enabled !== false;
const confirmationTitle = isEnabled ? 'Disable Auto-Sync?' : 'Enable Auto-Sync?';
const confirmationText = isEnabled
? 'Are you sure you want to disable automated application synchronization'
: 'If checked, application will automatically sync when changes are detected';
const confirmed = await ctx.popup.confirm(confirmationTitle, confirmationText);
if (confirmed) {
try {
setChangeSync(true);
const canUpdate = await services.accounts.canI('applications', 'update', appRBACName(app)).catch(() => false);
if (canUpdate) {
const updatedApp = JSON.parse(JSON.stringify(props.app)) as models.Application;
if (!updatedApp.spec.syncPolicy) {
updatedApp.spec.syncPolicy = {};
}
const existingAutomated = updatedApp.spec.syncPolicy.automated;
updatedApp.spec.syncPolicy.automated = {
prune: existingAutomated?.prune ?? false,
selfHeal: existingAutomated?.selfHeal ?? false,
enabled: !isEnabled
};
await updateApp(updatedApp, {validate: false});
} else {
await services.applications.runResourceAction(
app.metadata.name,
app.metadata.namespace,
{
name: app.metadata.name,
namespace: app.metadata.namespace,
group: 'argoproj.io',
kind: 'Application',
version: 'v1alpha1'
} as models.ResourceNode,
'toggle-auto-sync',
[]
);
}
} catch (e) {
ctx.notifications.show({
content: <ErrorNotification title={`Unable to "${confirmationTitle.replace(/\?/g, '')}"`} e={e} />,
type: NotificationType.Error
});
} finally {
setChangeSync(false);
}
}
}
// Handler for the PRUNE RESOURCES and SELF HEAL checkboxes only; auto-sync toggling lives in `toggleAutoSync` above.
async function setAutoSync(ctx: ContextApis, confirmationTitle: string, confirmationText: string, prune: boolean, selfHeal: boolean, enable: boolean) {
const confirmed = await ctx.popup.confirm(confirmationTitle, confirmationText);
if (confirmed) {
@ -542,7 +592,7 @@ export const ApplicationSummary = (props: ApplicationSummaryProps) => {
await updateApp(updatedApp, {validate: false});
} catch (e) {
ctx.notifications.show({
content: <ErrorNotification title={`Unable to "${confirmationTitle.replace(/\?/g, '')}:`} e={e} />,
content: <ErrorNotification title={`Unable to "${confirmationTitle.replace(/\?/g, '')}"`} e={e} />,
type: NotificationType.Error
});
} finally {
@ -663,18 +713,8 @@ export const ApplicationSummary = (props: ApplicationSummaryProps) => {
<div className='columns small-12'>
<div className='checkbox-container'>
<Checkbox
onChange={async (val: boolean) => {
const automated = app.spec.syncPolicy?.automated || {prune: false, selfHeal: false};
setAutoSync(
ctx,
val ? 'Enable Auto-Sync?' : 'Disable Auto-Sync?',
val
? 'If checked, application will automatically sync when changes are detected'
: 'Are you sure you want to disable automated application synchronization',
automated.prune,
automated.selfHeal,
val
);
onChange={async () => {
await toggleAutoSync(ctx);
}}
checked={app.spec.syncPolicy?.automated && app.spec.syncPolicy.automated.enabled !== false}
id='enable-auto-sync'

View file

@ -397,15 +397,16 @@ func (c *diffConfig) DiffFromCache(appName string) (bool, []*v1alpha1.ResourceDi
return false, nil
}
cachedDiff := make([]*v1alpha1.ResourceDiff, 0)
if c.stateCache != nil {
err := c.stateCache.GetAppManagedResources(appName, &cachedDiff)
if err != nil {
log.Errorf("DiffFromCache error: error getting managed resources for app %s: %s", appName, err)
return false, nil
err := c.stateCache.GetAppManagedResources(appName, &cachedDiff)
if err != nil {
if errors.Is(err, appstatecache.ErrCacheMiss) {
log.Warnf("DiffFromCache: cannot get managed resources for app %s: %s", appName, err)
} else {
log.Errorf("DiffFromCache: cannot get managed resources for app %s: %s", appName, err)
}
return true, cachedDiff
return false, nil
}
return false, nil
return true, cachedDiff
}
// preDiffNormalize applies the normalization of live and target resources before invoking

View file

@ -1,8 +1,11 @@
package diff_test
import (
"errors"
"testing"
"github.com/sirupsen/logrus"
"github.com/sirupsen/logrus/hooks/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@ -12,6 +15,7 @@ import (
argo "github.com/argoproj/argo-cd/v3/util/argo/diff"
"github.com/argoproj/argo-cd/v3/util/argo/normalizers"
"github.com/argoproj/argo-cd/v3/util/argo/testdata"
cacheutil "github.com/argoproj/argo-cd/v3/util/cache"
appstatecache "github.com/argoproj/argo-cd/v3/util/cache/appstate"
)
@ -248,3 +252,74 @@ func TestDiffConfigBuilder(t *testing.T) {
require.Nil(t, diffConfig)
})
}
func TestDiffFromCache(t *testing.T) {
t.Run("returns false and logs warning on cache miss", func(t *testing.T) {
// given
hook := test.NewLocal(logrus.StandardLogger())
defer hook.Reset()
// Real in-memory cache with no data stored → triggers ErrCacheMiss
cache := appstatecache.NewCache(cacheutil.NewCache(cacheutil.NewInMemoryCache(0)), 0)
diffConfig, err := argo.NewDiffConfigBuilder().
WithDiffSettings([]v1alpha1.ResourceIgnoreDifferences{}, map[string]v1alpha1.ResourceOverride{}, false, normalizers.IgnoreNormalizerOpts{}).
WithTracking("", "").
WithCache(cache, "application-name").
Build()
require.NoError(t, err)
// when
found, cachedDiff := diffConfig.DiffFromCache("application-name")
// then
assert.False(t, found)
assert.Nil(t, cachedDiff)
require.Len(t, hook.Entries, 1)
assert.Equal(t, logrus.WarnLevel, hook.LastEntry().Level)
assert.Contains(t, hook.LastEntry().Message, "cannot get managed resources for app application-name")
assert.Contains(t, hook.LastEntry().Message, appstatecache.ErrCacheMiss.Error())
})
t.Run("returns false and logs error on cache failure", func(t *testing.T) {
// given
hook := test.NewLocal(logrus.StandardLogger())
defer hook.Reset()
errCache := errors.New("cache unavailable")
// Custom cache client that always returns the given error on Get
failClient := &failingCacheClient{
InMemoryCache: cacheutil.NewInMemoryCache(0),
err: errCache,
}
cache := appstatecache.NewCache(cacheutil.NewCache(failClient), 0)
diffConfig, err := argo.NewDiffConfigBuilder().
WithDiffSettings([]v1alpha1.ResourceIgnoreDifferences{}, map[string]v1alpha1.ResourceOverride{}, false, normalizers.IgnoreNormalizerOpts{}).
WithTracking("", "").
WithCache(cache, "application-name").
Build()
require.NoError(t, err)
// when
found, cachedDiff := diffConfig.DiffFromCache("application-name")
// then
assert.False(t, found)
assert.Nil(t, cachedDiff)
require.Len(t, hook.Entries, 1)
assert.Equal(t, logrus.ErrorLevel, hook.LastEntry().Level)
assert.Contains(t, hook.LastEntry().Message, "cannot get managed resources for app application-name")
assert.Contains(t, hook.LastEntry().Message, errCache.Error())
})
}
// failingCacheClient embeds InMemoryCache and overrides Get to always return a custom error.
type failingCacheClient struct {
*cacheutil.InMemoryCache
err error
}
func (f *failingCacheClient) Get(_ string, _ any) error {
return f.err
}