fix(hydrator): normalize repo URL when grouping (#23564) (#23565)

Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>
This commit is contained in:
Michael Crenshaw
2025-07-01 12:33:04 -04:00
committed by GitHub
parent c60a727524
commit 4d16fdcea4
8 changed files with 601 additions and 21 deletions

View File

@@ -32,6 +32,9 @@ packages:
github.com/argoproj/argo-cd/v3/controller/cache:
interfaces:
LiveStateCache: {}
github.com/argoproj/argo-cd/v3/controller/hydrator:
interfaces:
Dependencies: {}
github.com/argoproj/argo-cd/v3/pkg/apiclient/cluster:
interfaces:
ClusterServiceServer: {}

View File

@@ -47,6 +47,7 @@ import (
"github.com/argoproj/argo-cd/v3/common"
statecache "github.com/argoproj/argo-cd/v3/controller/cache"
"github.com/argoproj/argo-cd/v3/controller/hydrator"
hydratortypes "github.com/argoproj/argo-cd/v3/controller/hydrator/types"
"github.com/argoproj/argo-cd/v3/controller/metrics"
"github.com/argoproj/argo-cd/v3/controller/sharding"
"github.com/argoproj/argo-cd/v3/pkg/apis/application"
@@ -115,7 +116,7 @@ type ApplicationController struct {
appOperationQueue workqueue.TypedRateLimitingInterface[string]
projectRefreshQueue workqueue.TypedRateLimitingInterface[string]
appHydrateQueue workqueue.TypedRateLimitingInterface[string]
hydrationQueue workqueue.TypedRateLimitingInterface[hydrator.HydrationQueueKey]
hydrationQueue workqueue.TypedRateLimitingInterface[hydratortypes.HydrationQueueKey]
appInformer cache.SharedIndexInformer
appLister applisters.ApplicationLister
projInformer cache.SharedIndexInformer
@@ -198,7 +199,7 @@ func NewApplicationController(
projectRefreshQueue: workqueue.NewTypedRateLimitingQueueWithConfig(ratelimiter.NewCustomAppControllerRateLimiter[string](rateLimiterConfig), workqueue.TypedRateLimitingQueueConfig[string]{Name: "project_reconciliation_queue"}),
appComparisonTypeRefreshQueue: workqueue.NewTypedRateLimitingQueue(ratelimiter.NewCustomAppControllerRateLimiter[string](rateLimiterConfig)),
appHydrateQueue: workqueue.NewTypedRateLimitingQueueWithConfig(ratelimiter.NewCustomAppControllerRateLimiter[string](rateLimiterConfig), workqueue.TypedRateLimitingQueueConfig[string]{Name: "app_hydration_queue"}),
hydrationQueue: workqueue.NewTypedRateLimitingQueueWithConfig(ratelimiter.NewCustomAppControllerRateLimiter[hydrator.HydrationQueueKey](rateLimiterConfig), workqueue.TypedRateLimitingQueueConfig[hydrator.HydrationQueueKey]{Name: "manifest_hydration_queue"}),
hydrationQueue: workqueue.NewTypedRateLimitingQueueWithConfig(ratelimiter.NewCustomAppControllerRateLimiter[hydratortypes.HydrationQueueKey](rateLimiterConfig), workqueue.TypedRateLimitingQueueConfig[hydratortypes.HydrationQueueKey]{Name: "manifest_hydration_queue"}),
db: db,
statusRefreshTimeout: appResyncPeriod,
statusHardRefreshTimeout: appHardResyncPeriod,

View File

@@ -11,12 +11,16 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
commitclient "github.com/argoproj/argo-cd/v3/commitserver/apiclient"
"github.com/argoproj/argo-cd/v3/controller/hydrator/types"
appv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v3/reposerver/apiclient"
applog "github.com/argoproj/argo-cd/v3/util/app/log"
"github.com/argoproj/argo-cd/v3/util/git"
utilio "github.com/argoproj/argo-cd/v3/util/io"
)
// RepoGetter is an interface that defines methods for getting repository objects. It's a subset of the DB interface to
// avoid granting access to things we don't need.
type RepoGetter interface {
// GetRepository returns a repository by its URL and project name.
GetRepository(ctx context.Context, repoURL, project string) (*appv1.Repository, error)
@@ -28,16 +32,37 @@ type RepoGetter interface {
type Dependencies interface {
// TODO: determine if we actually need to get the app, or if all the stuff we need the app for is done already on
// the app controller side.
// GetProcessableAppProj returns the AppProject for the given application. It should only return projects that are
// processable by the controller, meaning that the project is not deleted and the application is in a namespace
// permitted by the project.
GetProcessableAppProj(app *appv1.Application) (*appv1.AppProject, error)
// GetProcessableApps returns a list of applications that are processable by the controller.
GetProcessableApps() (*appv1.ApplicationList, error)
// GetRepoObjs returns the repository objects for the given application, source, and revision. It calls the repo-
// server and gets the manifests (objects).
GetRepoObjs(app *appv1.Application, source appv1.ApplicationSource, revision string, project *appv1.AppProject) ([]*unstructured.Unstructured, *apiclient.ManifestResponse, error)
// GetWriteCredentials returns the repository credentials for the given repository URL and project. These are to be
// sent to the commit server to write the hydrated manifests.
GetWriteCredentials(ctx context.Context, repoURL string, project string) (*appv1.Repository, error)
// RequestAppRefresh requests a refresh of the application with the given name and namespace. This is used to
// trigger a refresh after the application has been hydrated and a new commit has been pushed.
RequestAppRefresh(appName string, appNamespace string) error
// TODO: only allow access to the hydrator status
// PersistAppHydratorStatus persists the application status for the source hydrator.
PersistAppHydratorStatus(orig *appv1.Application, newStatus *appv1.SourceHydratorStatus)
AddHydrationQueueItem(key HydrationQueueKey)
// AddHydrationQueueItem adds a hydration queue item to the queue. This is used to trigger the hydration process for
// a group of applications which are hydrating to the same repo and target branch.
AddHydrationQueueItem(key types.HydrationQueueKey)
}
// Hydrator is the main struct that implements the hydration logic. It uses the Dependencies interface to access the
// app controller's functionality without directly depending on it.
type Hydrator struct {
dependencies Dependencies
statusRefreshTimeout time.Duration
@@ -46,6 +71,9 @@ type Hydrator struct {
repoGetter RepoGetter
}
// NewHydrator creates a new Hydrator instance with the given dependencies, status refresh timeout, commit clientset,
// repo clientset, and repo getter. The refresh timeout determines how often the hydrator checks if an application
// needs to be hydrated.
func NewHydrator(dependencies Dependencies, statusRefreshTimeout time.Duration, commitClientset commitclient.Clientset, repoClientset apiclient.Clientset, repoGetter RepoGetter) *Hydrator {
return &Hydrator{
dependencies: dependencies,
@@ -56,6 +84,12 @@ func NewHydrator(dependencies Dependencies, statusRefreshTimeout time.Duration,
}
}
// ProcessAppHydrateQueueItem processes an application hydrate queue item. It checks if the application needs hydration
// and if so, it updates the application's status to indicate that hydration is in progress. It then adds the
// hydration queue item to the queue for further processing.
//
// It's likely that multiple applications will trigger hydration at the same time. The hydration queue key is meant to
// dedupe these requests.
func (h *Hydrator) ProcessAppHydrateQueueItem(origApp *appv1.Application) {
origApp = origApp.DeepCopy()
app := origApp.DeepCopy()
@@ -89,27 +123,24 @@ func (h *Hydrator) ProcessAppHydrateQueueItem(origApp *appv1.Application) {
logCtx.Debug("Successfully processed app hydrate queue item")
}
func getHydrationQueueKey(app *appv1.Application) HydrationQueueKey {
func getHydrationQueueKey(app *appv1.Application) types.HydrationQueueKey {
destinationBranch := app.Spec.SourceHydrator.SyncSource.TargetBranch
if app.Spec.SourceHydrator.HydrateTo != nil {
destinationBranch = app.Spec.SourceHydrator.HydrateTo.TargetBranch
}
key := HydrationQueueKey{
SourceRepoURL: app.Spec.SourceHydrator.DrySource.RepoURL,
key := types.HydrationQueueKey{
SourceRepoURL: git.NormalizeGitURLAllowInvalid(app.Spec.SourceHydrator.DrySource.RepoURL),
SourceTargetRevision: app.Spec.SourceHydrator.DrySource.TargetRevision,
DestinationBranch: destinationBranch,
}
return key
}
type HydrationQueueKey struct {
SourceRepoURL string
SourceTargetRevision string
DestinationBranch string
}
// uniqueHydrationDestination is used to detect duplicate hydrate destinations.
type uniqueHydrationDestination struct {
// sourceRepoURL must be normalized with git.NormalizeGitURL to ensure that two apps with different URL formats
// don't end up in two different hydration queue items. Failing to normalize would result in one hydrated commit for
// each unique URL.
//nolint:unused // used as part of a map key
sourceRepoURL string
//nolint:unused // used as part of a map key
@@ -120,7 +151,11 @@ type uniqueHydrationDestination struct {
destinationPath string
}
func (h *Hydrator) ProcessHydrationQueueItem(hydrationKey HydrationQueueKey) (processNext bool) {
// ProcessHydrationQueueItem processes a hydration queue item. It retrieves the relevant applications for the given
// hydration key, hydrates their latest commit, and updates their status accordingly. If the hydration fails, it marks
// the operation as failed and logs the error. If successful, it updates the operation to indicate that hydration was
// successful and requests a refresh of the applications to pick up the new hydrated commit.
func (h *Hydrator) ProcessHydrationQueueItem(hydrationKey types.HydrationQueueKey) (processNext bool) {
logCtx := log.WithFields(log.Fields{
"sourceRepoURL": hydrationKey.SourceRepoURL,
"sourceTargetRevision": hydrationKey.SourceTargetRevision,
@@ -177,7 +212,7 @@ func (h *Hydrator) ProcessHydrationQueueItem(hydrationKey HydrationQueueKey) (pr
return
}
func (h *Hydrator) hydrateAppsLatestCommit(logCtx *log.Entry, hydrationKey HydrationQueueKey) ([]*appv1.Application, string, string, error) {
func (h *Hydrator) hydrateAppsLatestCommit(logCtx *log.Entry, hydrationKey types.HydrationQueueKey) ([]*appv1.Application, string, string, error) {
relevantApps, err := h.getRelevantAppsForHydration(logCtx, hydrationKey)
if err != nil {
return nil, "", "", fmt.Errorf("failed to get relevant apps for hydration: %w", err)
@@ -191,7 +226,7 @@ func (h *Hydrator) hydrateAppsLatestCommit(logCtx *log.Entry, hydrationKey Hydra
return relevantApps, dryRevision, hydratedRevision, nil
}
func (h *Hydrator) getRelevantAppsForHydration(logCtx *log.Entry, hydrationKey HydrationQueueKey) ([]*appv1.Application, error) {
func (h *Hydrator) getRelevantAppsForHydration(logCtx *log.Entry, hydrationKey types.HydrationQueueKey) ([]*appv1.Application, error) {
// Get all apps
apps, err := h.dependencies.GetProcessableApps()
if err != nil {
@@ -205,7 +240,7 @@ func (h *Hydrator) getRelevantAppsForHydration(logCtx *log.Entry, hydrationKey H
continue
}
if app.Spec.SourceHydrator.DrySource.RepoURL != hydrationKey.SourceRepoURL ||
if !git.SameURL(app.Spec.SourceHydrator.DrySource.RepoURL, hydrationKey.SourceRepoURL) ||
app.Spec.SourceHydrator.DrySource.TargetRevision != hydrationKey.SourceTargetRevision {
continue
}
@@ -230,7 +265,7 @@ func (h *Hydrator) getRelevantAppsForHydration(logCtx *log.Entry, hydrationKey H
}
uniqueDestinationKey := uniqueHydrationDestination{
sourceRepoURL: app.Spec.SourceHydrator.DrySource.RepoURL,
sourceRepoURL: git.NormalizeGitURLAllowInvalid(app.Spec.SourceHydrator.DrySource.RepoURL),
sourceTargetRevision: app.Spec.SourceHydrator.DrySource.TargetRevision,
destinationBranch: destinationBranch,
destinationPath: app.Spec.SourceHydrator.SyncSource.Path,

View File

@@ -4,9 +4,14 @@ import (
"testing"
"time"
log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/argoproj/argo-cd/v3/controller/hydrator/mocks"
"github.com/argoproj/argo-cd/v3/controller/hydrator/types"
"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
)
@@ -101,3 +106,64 @@ func Test_appNeedsHydration(t *testing.T) {
})
}
}
func Test_getRelevantAppsForHydration_RepoURLNormalization(t *testing.T) {
t.Parallel()
d := mocks.NewDependencies(t)
d.On("GetProcessableApps").Return(&v1alpha1.ApplicationList{
Items: []v1alpha1.Application{
{
Spec: v1alpha1.ApplicationSpec{
Project: "project",
SourceHydrator: &v1alpha1.SourceHydrator{
DrySource: v1alpha1.DrySource{
RepoURL: "https://example.com/repo.git",
TargetRevision: "main",
Path: "app1",
},
SyncSource: v1alpha1.SyncSource{
TargetBranch: "main",
Path: "app1",
},
},
},
},
{
Spec: v1alpha1.ApplicationSpec{
Project: "project",
SourceHydrator: &v1alpha1.SourceHydrator{
DrySource: v1alpha1.DrySource{
RepoURL: "https://example.com/repo",
TargetRevision: "main",
Path: "app2",
},
SyncSource: v1alpha1.SyncSource{
TargetBranch: "main",
Path: "app2",
},
},
},
},
},
}, nil)
d.On("GetProcessableAppProj", mock.Anything).Return(&v1alpha1.AppProject{
Spec: v1alpha1.AppProjectSpec{
SourceRepos: []string{"https://example.com/*"},
},
}, nil)
hydrator := &Hydrator{dependencies: d}
hydrationKey := types.HydrationQueueKey{
SourceRepoURL: "https://example.com/repo",
SourceTargetRevision: "main",
DestinationBranch: "main",
}
logCtx := log.WithField("test", "RepoURLNormalization")
relevantApps, err := hydrator.getRelevantAppsForHydration(logCtx, hydrationKey)
require.NoError(t, err)
assert.Len(t, relevantApps, 2, "Expected both apps to be considered relevant despite URL differences")
}

464
controller/hydrator/mocks/Dependencies.go generated Normal file
View File

@@ -0,0 +1,464 @@
// Code generated by mockery; DO NOT EDIT.
// github.com/vektra/mockery
// template: testify
package mocks
import (
"context"
"github.com/argoproj/argo-cd/v3/controller/hydrator/types"
"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v3/reposerver/apiclient"
mock "github.com/stretchr/testify/mock"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
// NewDependencies creates a new instance of Dependencies. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewDependencies(t interface {
mock.TestingT
Cleanup(func())
}) *Dependencies {
mock := &Dependencies{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
// Dependencies is an autogenerated mock type for the Dependencies type
type Dependencies struct {
mock.Mock
}
type Dependencies_Expecter struct {
mock *mock.Mock
}
func (_m *Dependencies) EXPECT() *Dependencies_Expecter {
return &Dependencies_Expecter{mock: &_m.Mock}
}
// AddHydrationQueueItem provides a mock function for the type Dependencies
func (_mock *Dependencies) AddHydrationQueueItem(key types.HydrationQueueKey) {
_mock.Called(key)
return
}
// Dependencies_AddHydrationQueueItem_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddHydrationQueueItem'
type Dependencies_AddHydrationQueueItem_Call struct {
*mock.Call
}
// AddHydrationQueueItem is a helper method to define mock.On call
// - key types.HydrationQueueKey
func (_e *Dependencies_Expecter) AddHydrationQueueItem(key interface{}) *Dependencies_AddHydrationQueueItem_Call {
return &Dependencies_AddHydrationQueueItem_Call{Call: _e.mock.On("AddHydrationQueueItem", key)}
}
func (_c *Dependencies_AddHydrationQueueItem_Call) Run(run func(key types.HydrationQueueKey)) *Dependencies_AddHydrationQueueItem_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 types.HydrationQueueKey
if args[0] != nil {
arg0 = args[0].(types.HydrationQueueKey)
}
run(
arg0,
)
})
return _c
}
func (_c *Dependencies_AddHydrationQueueItem_Call) Return() *Dependencies_AddHydrationQueueItem_Call {
_c.Call.Return()
return _c
}
func (_c *Dependencies_AddHydrationQueueItem_Call) RunAndReturn(run func(key types.HydrationQueueKey)) *Dependencies_AddHydrationQueueItem_Call {
_c.Run(run)
return _c
}
// GetProcessableAppProj provides a mock function for the type Dependencies
func (_mock *Dependencies) GetProcessableAppProj(app *v1alpha1.Application) (*v1alpha1.AppProject, error) {
ret := _mock.Called(app)
if len(ret) == 0 {
panic("no return value specified for GetProcessableAppProj")
}
var r0 *v1alpha1.AppProject
var r1 error
if returnFunc, ok := ret.Get(0).(func(*v1alpha1.Application) (*v1alpha1.AppProject, error)); ok {
return returnFunc(app)
}
if returnFunc, ok := ret.Get(0).(func(*v1alpha1.Application) *v1alpha1.AppProject); ok {
r0 = returnFunc(app)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*v1alpha1.AppProject)
}
}
if returnFunc, ok := ret.Get(1).(func(*v1alpha1.Application) error); ok {
r1 = returnFunc(app)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Dependencies_GetProcessableAppProj_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetProcessableAppProj'
type Dependencies_GetProcessableAppProj_Call struct {
*mock.Call
}
// GetProcessableAppProj is a helper method to define mock.On call
// - app *v1alpha1.Application
func (_e *Dependencies_Expecter) GetProcessableAppProj(app interface{}) *Dependencies_GetProcessableAppProj_Call {
return &Dependencies_GetProcessableAppProj_Call{Call: _e.mock.On("GetProcessableAppProj", app)}
}
func (_c *Dependencies_GetProcessableAppProj_Call) Run(run func(app *v1alpha1.Application)) *Dependencies_GetProcessableAppProj_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 *v1alpha1.Application
if args[0] != nil {
arg0 = args[0].(*v1alpha1.Application)
}
run(
arg0,
)
})
return _c
}
func (_c *Dependencies_GetProcessableAppProj_Call) Return(appProject *v1alpha1.AppProject, err error) *Dependencies_GetProcessableAppProj_Call {
_c.Call.Return(appProject, err)
return _c
}
func (_c *Dependencies_GetProcessableAppProj_Call) RunAndReturn(run func(app *v1alpha1.Application) (*v1alpha1.AppProject, error)) *Dependencies_GetProcessableAppProj_Call {
_c.Call.Return(run)
return _c
}
// GetProcessableApps provides a mock function for the type Dependencies
func (_mock *Dependencies) GetProcessableApps() (*v1alpha1.ApplicationList, error) {
ret := _mock.Called()
if len(ret) == 0 {
panic("no return value specified for GetProcessableApps")
}
var r0 *v1alpha1.ApplicationList
var r1 error
if returnFunc, ok := ret.Get(0).(func() (*v1alpha1.ApplicationList, error)); ok {
return returnFunc()
}
if returnFunc, ok := ret.Get(0).(func() *v1alpha1.ApplicationList); ok {
r0 = returnFunc()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*v1alpha1.ApplicationList)
}
}
if returnFunc, ok := ret.Get(1).(func() error); ok {
r1 = returnFunc()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Dependencies_GetProcessableApps_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetProcessableApps'
type Dependencies_GetProcessableApps_Call struct {
*mock.Call
}
// GetProcessableApps is a helper method to define mock.On call
func (_e *Dependencies_Expecter) GetProcessableApps() *Dependencies_GetProcessableApps_Call {
return &Dependencies_GetProcessableApps_Call{Call: _e.mock.On("GetProcessableApps")}
}
func (_c *Dependencies_GetProcessableApps_Call) Run(run func()) *Dependencies_GetProcessableApps_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *Dependencies_GetProcessableApps_Call) Return(applicationList *v1alpha1.ApplicationList, err error) *Dependencies_GetProcessableApps_Call {
_c.Call.Return(applicationList, err)
return _c
}
func (_c *Dependencies_GetProcessableApps_Call) RunAndReturn(run func() (*v1alpha1.ApplicationList, error)) *Dependencies_GetProcessableApps_Call {
_c.Call.Return(run)
return _c
}
// GetRepoObjs provides a mock function for the type Dependencies
func (_mock *Dependencies) GetRepoObjs(app *v1alpha1.Application, source v1alpha1.ApplicationSource, revision string, project *v1alpha1.AppProject) ([]*unstructured.Unstructured, *apiclient.ManifestResponse, error) {
ret := _mock.Called(app, source, revision, project)
if len(ret) == 0 {
panic("no return value specified for GetRepoObjs")
}
var r0 []*unstructured.Unstructured
var r1 *apiclient.ManifestResponse
var r2 error
if returnFunc, ok := ret.Get(0).(func(*v1alpha1.Application, v1alpha1.ApplicationSource, string, *v1alpha1.AppProject) ([]*unstructured.Unstructured, *apiclient.ManifestResponse, error)); ok {
return returnFunc(app, source, revision, project)
}
if returnFunc, ok := ret.Get(0).(func(*v1alpha1.Application, v1alpha1.ApplicationSource, string, *v1alpha1.AppProject) []*unstructured.Unstructured); ok {
r0 = returnFunc(app, source, revision, project)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*unstructured.Unstructured)
}
}
if returnFunc, ok := ret.Get(1).(func(*v1alpha1.Application, v1alpha1.ApplicationSource, string, *v1alpha1.AppProject) *apiclient.ManifestResponse); ok {
r1 = returnFunc(app, source, revision, project)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*apiclient.ManifestResponse)
}
}
if returnFunc, ok := ret.Get(2).(func(*v1alpha1.Application, v1alpha1.ApplicationSource, string, *v1alpha1.AppProject) error); ok {
r2 = returnFunc(app, source, revision, project)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// Dependencies_GetRepoObjs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetRepoObjs'
type Dependencies_GetRepoObjs_Call struct {
*mock.Call
}
// GetRepoObjs is a helper method to define mock.On call
// - app *v1alpha1.Application
// - source v1alpha1.ApplicationSource
// - revision string
// - project *v1alpha1.AppProject
func (_e *Dependencies_Expecter) GetRepoObjs(app interface{}, source interface{}, revision interface{}, project interface{}) *Dependencies_GetRepoObjs_Call {
return &Dependencies_GetRepoObjs_Call{Call: _e.mock.On("GetRepoObjs", app, source, revision, project)}
}
func (_c *Dependencies_GetRepoObjs_Call) Run(run func(app *v1alpha1.Application, source v1alpha1.ApplicationSource, revision string, project *v1alpha1.AppProject)) *Dependencies_GetRepoObjs_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 *v1alpha1.Application
if args[0] != nil {
arg0 = args[0].(*v1alpha1.Application)
}
var arg1 v1alpha1.ApplicationSource
if args[1] != nil {
arg1 = args[1].(v1alpha1.ApplicationSource)
}
var arg2 string
if args[2] != nil {
arg2 = args[2].(string)
}
var arg3 *v1alpha1.AppProject
if args[3] != nil {
arg3 = args[3].(*v1alpha1.AppProject)
}
run(
arg0,
arg1,
arg2,
arg3,
)
})
return _c
}
func (_c *Dependencies_GetRepoObjs_Call) Return(unstructureds []*unstructured.Unstructured, manifestResponse *apiclient.ManifestResponse, err error) *Dependencies_GetRepoObjs_Call {
_c.Call.Return(unstructureds, manifestResponse, err)
return _c
}
func (_c *Dependencies_GetRepoObjs_Call) RunAndReturn(run func(app *v1alpha1.Application, source v1alpha1.ApplicationSource, revision string, project *v1alpha1.AppProject) ([]*unstructured.Unstructured, *apiclient.ManifestResponse, error)) *Dependencies_GetRepoObjs_Call {
_c.Call.Return(run)
return _c
}
// GetWriteCredentials provides a mock function for the type Dependencies
func (_mock *Dependencies) GetWriteCredentials(ctx context.Context, repoURL string, project string) (*v1alpha1.Repository, error) {
ret := _mock.Called(ctx, repoURL, project)
if len(ret) == 0 {
panic("no return value specified for GetWriteCredentials")
}
var r0 *v1alpha1.Repository
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) (*v1alpha1.Repository, error)); ok {
return returnFunc(ctx, repoURL, project)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) *v1alpha1.Repository); ok {
r0 = returnFunc(ctx, repoURL, project)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*v1alpha1.Repository)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
r1 = returnFunc(ctx, repoURL, project)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Dependencies_GetWriteCredentials_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetWriteCredentials'
type Dependencies_GetWriteCredentials_Call struct {
*mock.Call
}
// GetWriteCredentials is a helper method to define mock.On call
// - ctx context.Context
// - repoURL string
// - project string
func (_e *Dependencies_Expecter) GetWriteCredentials(ctx interface{}, repoURL interface{}, project interface{}) *Dependencies_GetWriteCredentials_Call {
return &Dependencies_GetWriteCredentials_Call{Call: _e.mock.On("GetWriteCredentials", ctx, repoURL, project)}
}
func (_c *Dependencies_GetWriteCredentials_Call) Run(run func(ctx context.Context, repoURL string, project string)) *Dependencies_GetWriteCredentials_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 string
if args[1] != nil {
arg1 = args[1].(string)
}
var arg2 string
if args[2] != nil {
arg2 = args[2].(string)
}
run(
arg0,
arg1,
arg2,
)
})
return _c
}
func (_c *Dependencies_GetWriteCredentials_Call) Return(repository *v1alpha1.Repository, err error) *Dependencies_GetWriteCredentials_Call {
_c.Call.Return(repository, err)
return _c
}
func (_c *Dependencies_GetWriteCredentials_Call) RunAndReturn(run func(ctx context.Context, repoURL string, project string) (*v1alpha1.Repository, error)) *Dependencies_GetWriteCredentials_Call {
_c.Call.Return(run)
return _c
}
// PersistAppHydratorStatus provides a mock function for the type Dependencies
func (_mock *Dependencies) PersistAppHydratorStatus(orig *v1alpha1.Application, newStatus *v1alpha1.SourceHydratorStatus) {
_mock.Called(orig, newStatus)
return
}
// Dependencies_PersistAppHydratorStatus_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PersistAppHydratorStatus'
type Dependencies_PersistAppHydratorStatus_Call struct {
*mock.Call
}
// PersistAppHydratorStatus is a helper method to define mock.On call
// - orig *v1alpha1.Application
// - newStatus *v1alpha1.SourceHydratorStatus
func (_e *Dependencies_Expecter) PersistAppHydratorStatus(orig interface{}, newStatus interface{}) *Dependencies_PersistAppHydratorStatus_Call {
return &Dependencies_PersistAppHydratorStatus_Call{Call: _e.mock.On("PersistAppHydratorStatus", orig, newStatus)}
}
func (_c *Dependencies_PersistAppHydratorStatus_Call) Run(run func(orig *v1alpha1.Application, newStatus *v1alpha1.SourceHydratorStatus)) *Dependencies_PersistAppHydratorStatus_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 *v1alpha1.Application
if args[0] != nil {
arg0 = args[0].(*v1alpha1.Application)
}
var arg1 *v1alpha1.SourceHydratorStatus
if args[1] != nil {
arg1 = args[1].(*v1alpha1.SourceHydratorStatus)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *Dependencies_PersistAppHydratorStatus_Call) Return() *Dependencies_PersistAppHydratorStatus_Call {
_c.Call.Return()
return _c
}
func (_c *Dependencies_PersistAppHydratorStatus_Call) RunAndReturn(run func(orig *v1alpha1.Application, newStatus *v1alpha1.SourceHydratorStatus)) *Dependencies_PersistAppHydratorStatus_Call {
_c.Run(run)
return _c
}
// RequestAppRefresh provides a mock function for the type Dependencies
func (_mock *Dependencies) RequestAppRefresh(appName string, appNamespace string) error {
ret := _mock.Called(appName, appNamespace)
if len(ret) == 0 {
panic("no return value specified for RequestAppRefresh")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(string, string) error); ok {
r0 = returnFunc(appName, appNamespace)
} else {
r0 = ret.Error(0)
}
return r0
}
// Dependencies_RequestAppRefresh_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RequestAppRefresh'
type Dependencies_RequestAppRefresh_Call struct {
*mock.Call
}
// RequestAppRefresh is a helper method to define mock.On call
// - appName string
// - appNamespace string
func (_e *Dependencies_Expecter) RequestAppRefresh(appName interface{}, appNamespace interface{}) *Dependencies_RequestAppRefresh_Call {
return &Dependencies_RequestAppRefresh_Call{Call: _e.mock.On("RequestAppRefresh", appName, appNamespace)}
}
func (_c *Dependencies_RequestAppRefresh_Call) Run(run func(appName string, appNamespace string)) *Dependencies_RequestAppRefresh_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 string
if args[0] != nil {
arg0 = args[0].(string)
}
var arg1 string
if args[1] != nil {
arg1 = args[1].(string)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *Dependencies_RequestAppRefresh_Call) Return(err error) *Dependencies_RequestAppRefresh_Call {
_c.Call.Return(err)
return _c
}
func (_c *Dependencies_RequestAppRefresh_Call) RunAndReturn(run func(appName string, appNamespace string) error) *Dependencies_RequestAppRefresh_Call {
_c.Call.Return(run)
return _c
}

View File

@@ -0,0 +1,11 @@
package types
// HydrationQueueKey is used to uniquely identify a hydration operation in the queue. If several applications request
// hydration, but they have the same queue key, only one hydration operation will be performed.
type HydrationQueueKey struct {
// SourceRepoURL must be normalized with git.NormalizeGitURL to ensure that we don't double-queue a single hydration
// operation because two apps have different URL formats.
SourceRepoURL string
SourceTargetRevision string
DestinationBranch string
}

View File

@@ -4,7 +4,7 @@ import (
"context"
"fmt"
"github.com/argoproj/argo-cd/v3/controller/hydrator"
"github.com/argoproj/argo-cd/v3/controller/hydrator/types"
appv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v3/reposerver/apiclient"
argoutil "github.com/argoproj/argo-cd/v3/util/argo"
@@ -85,6 +85,6 @@ func (ctrl *ApplicationController) PersistAppHydratorStatus(orig *appv1.Applicat
ctrl.persistAppStatus(orig, status)
}
func (ctrl *ApplicationController) AddHydrationQueueItem(key hydrator.HydrationQueueKey) {
func (ctrl *ApplicationController) AddHydrationQueueItem(key types.HydrationQueueKey) {
ctrl.hydrationQueue.AddRateLimited(key)
}

View File

@@ -41,7 +41,7 @@ func SameURL(leftRepo, rightRepo string) bool {
return normalLeft != "" && normalRight != "" && normalLeft == normalRight
}
// Similar to NormalizeGitURL, except returning an original url if the url is invalid.
// NormalizeGitURLAllowInvalid is similar to NormalizeGitURL, except returning an original url if the url is invalid.
// Needed to allow a deletion of repos with invalid urls. See https://github.com/argoproj/argo-cd/issues/20921.
func NormalizeGitURLAllowInvalid(repo string) string {
normalized := NormalizeGitURL(repo)