Skip to content

Commit aba0fb0

Browse files
feat: handle duplicate files from multiple packages deterministically (cnoe-io#550)
Signed-off-by: suwhang-cisco <suwhang@cisco.com>
1 parent 95cfa34 commit aba0fb0

7 files changed

Lines changed: 589 additions & 18 deletions

File tree

api/v1alpha1/localbuild_types.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ const (
1212
LastObservedCLIStartTimeAnnotation = "cnoe.io/last-observed-cli-start-time"
1313
// CliStartTimeAnnotation indicates when the CLI was invoked.
1414
CliStartTimeAnnotation = "cnoe.io/cli-start-time"
15-
FieldManager = "idpbuilder"
15+
// PackagePriorityAnnotation indicates the priority of a package (higher = wins conflicts).
16+
PackagePriorityAnnotation = "cnoe.io/package-priority"
17+
// PackageSourcePathAnnotation indicates the source path of a package.
18+
PackageSourcePathAnnotation = "cnoe.io/package-source-path"
19+
FieldManager = "idpbuilder"
1620
// If GetSecretLabelKey is set to GetSecretLabelValue on a kubernetes secret, secret key and values can be used by the get command.
1721
CLISecretLabelKey = "cnoe.io/cli-secret"
1822
CLISecretLabelValue = "true"

pkg/controllers/custompackage/controller.go

Lines changed: 229 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,173 @@ func (r *Reconciler) postProcessReconcile(ctx context.Context, req ctrl.Request,
7272
}
7373
}
7474

75+
// shouldTakeOverGitRepository checks if this CustomPackage should take over an existing GitRepository.
76+
// Returns true if this package has higher priority than the current owner.
77+
func (r *Reconciler) shouldTakeOverGitRepository(ctx context.Context, resource *v1alpha1.CustomPackage, existingRepo *v1alpha1.GitRepository) (bool, error) {
78+
logger := log.FromContext(ctx)
79+
80+
// Get this package's priority
81+
thisPriority, err := getPackagePriority(resource)
82+
if err != nil {
83+
// If no priority annotation, assume it's a legacy package - allow takeover for backward compat
84+
logger.V(1).Info("no priority on this package, allowing takeover", "package", resource.Name)
85+
return true, nil
86+
}
87+
88+
// Check the existing repo's owner references
89+
for _, ownerRef := range existingRepo.GetOwnerReferences() {
90+
if ownerRef.Kind == "CustomPackage" {
91+
// Fetch the owner CustomPackage to get its priority
92+
ownerPkg := &v1alpha1.CustomPackage{}
93+
err := r.Client.Get(ctx, client.ObjectKey{
94+
Name: ownerRef.Name,
95+
Namespace: existingRepo.Namespace,
96+
}, ownerPkg)
97+
98+
if err != nil {
99+
if errors.IsNotFound(err) {
100+
// Owner doesn't exist anymore, we can take over
101+
logger.Info("GitRepository owner not found, taking over", "repo", existingRepo.Name)
102+
return true, nil
103+
}
104+
return false, fmt.Errorf("getting owner package: %w", err)
105+
}
106+
107+
// Get owner's priority
108+
ownerPriority, err := getPackagePriority(ownerPkg)
109+
if err != nil {
110+
// Owner has no priority, we can take over
111+
logger.V(1).Info("GitRepository owner has no priority, taking over", "repo", existingRepo.Name)
112+
return true, nil
113+
}
114+
115+
// Only take over if we have HIGHER priority (higher number wins)
116+
if thisPriority > ownerPriority {
117+
logger.Info("Taking over GitRepository from lower priority owner",
118+
"repo", existingRepo.Name,
119+
"ourPriority", thisPriority,
120+
"ownerPriority", ownerPriority,
121+
"owner", ownerPkg.Name)
122+
return true, nil
123+
} else {
124+
logger.Info("Not taking over GitRepository owned by higher/equal priority package",
125+
"repo", existingRepo.Name,
126+
"ourPriority", thisPriority,
127+
"ownerPriority", ownerPriority,
128+
"owner", ownerPkg.Name)
129+
return false, nil
130+
}
131+
}
132+
}
133+
134+
// No CustomPackage owner found, we can take over
135+
logger.V(1).Info("GitRepository has no CustomPackage owner, taking over", "repo", existingRepo.Name)
136+
return true, nil
137+
}
138+
139+
// shouldReconcile checks if this CustomPackage should proceed with reconciliation.
140+
// It returns true only if this is the highest priority CustomPackage for the same app.
141+
func (r *Reconciler) shouldReconcile(ctx context.Context, resource *v1alpha1.CustomPackage) (bool, error) {
142+
logger := log.FromContext(ctx)
143+
144+
// Get this package's priority
145+
thisPriority, err := getPackagePriority(resource)
146+
if err != nil {
147+
// If no priority annotation, assume it's a legacy package and allow it
148+
logger.V(1).Info("no priority annotation found, assuming legacy package", "name", resource.Name)
149+
return true, nil
150+
}
151+
152+
// List all CustomPackages in the same namespace
153+
pkgList := &v1alpha1.CustomPackageList{}
154+
err = r.Client.List(ctx, pkgList, client.InNamespace(resource.Namespace))
155+
if err != nil {
156+
return false, fmt.Errorf("listing custom packages: %w", err)
157+
}
158+
159+
// Check if any other CustomPackage has the same app name with higher priority
160+
for i := range pkgList.Items {
161+
pkg := &pkgList.Items[i]
162+
163+
// Skip self
164+
if pkg.Name == resource.Name {
165+
continue
166+
}
167+
168+
// Skip if different app name
169+
if pkg.Spec.ArgoCD.Name != resource.Spec.ArgoCD.Name {
170+
continue
171+
}
172+
173+
// Get the other package's priority
174+
otherPriority, err := getPackagePriority(pkg)
175+
if err != nil {
176+
// If the other package has no priority, this one wins
177+
continue
178+
}
179+
180+
// If another package has higher priority, this one should not reconcile
181+
if otherPriority > thisPriority {
182+
thisSource := resource.ObjectMeta.Annotations[v1alpha1.PackageSourcePathAnnotation]
183+
otherSource := pkg.ObjectMeta.Annotations[v1alpha1.PackageSourcePathAnnotation]
184+
logger.Info("Yielding to higher priority package - skipping all reconciliation",
185+
"appName", resource.Spec.ArgoCD.Name,
186+
"yieldingPackage", thisSource,
187+
"yieldingPriority", thisPriority,
188+
"activePackage", otherSource,
189+
"activePriority", otherPriority)
190+
return false, nil
191+
}
192+
}
193+
194+
return true, nil
195+
}
196+
197+
// getPackagePriority extracts the priority from a CustomPackage's annotations
198+
func getPackagePriority(pkg *v1alpha1.CustomPackage) (int, error) {
199+
if pkg.ObjectMeta.Annotations == nil {
200+
return 0, fmt.Errorf("no annotations")
201+
}
202+
203+
priorityStr, ok := pkg.ObjectMeta.Annotations[v1alpha1.PackagePriorityAnnotation]
204+
if !ok {
205+
return 0, fmt.Errorf("no priority annotation")
206+
}
207+
208+
var priority int
209+
_, err := fmt.Sscanf(priorityStr, "%d", &priority)
210+
if err != nil {
211+
return 0, fmt.Errorf("invalid priority format: %w", err)
212+
}
213+
214+
return priority, nil
215+
}
216+
75217
// create an in-cluster repository CR, update the application spec, then apply
76218
func (r *Reconciler) reconcileCustomPackage(ctx context.Context, resource *v1alpha1.CustomPackage) (ctrl.Result, error) {
219+
logger := log.FromContext(ctx)
220+
221+
// Check if this package should reconcile
222+
shouldReconcile, err := r.shouldReconcile(ctx, resource)
223+
if err != nil {
224+
return ctrl.Result{}, fmt.Errorf("checking if should reconcile: %w", err)
225+
}
226+
227+
if !shouldReconcile {
228+
// This package has been superseded by a higher priority package
229+
// Mark as not synced and don't update resources, and don't requeue
230+
logger.Info("package superseded by higher priority, skipping reconciliation",
231+
"name", resource.Name,
232+
"appName", resource.Spec.ArgoCD.Name,
233+
"sourcePath", resource.ObjectMeta.Annotations[v1alpha1.PackageSourcePathAnnotation])
234+
resource.Status.Synced = false
235+
return ctrl.Result{}, nil
236+
}
237+
238+
logger.V(1).Info("proceeding with reconciliation as highest priority package",
239+
"name", resource.Name,
240+
"appName", resource.Spec.ArgoCD.Name)
241+
77242
b, err := r.getArgoCDAppFile(ctx, resource)
78243
if err != nil {
79244
return ctrl.Result{}, fmt.Errorf("reading file %s: %w", resource.Spec.ArgoCD.ApplicationFile, err)
@@ -219,7 +384,12 @@ func (r *Reconciler) reconcileArgoCDApp(ctx context.Context, resource *v1alpha1.
219384
}
220385
resource.Status.GitRepositoryRefs = repoRefs
221386
resource.Status.Synced = appSourcesSynced
222-
return ctrl.Result{RequeueAfter: requeueTime}, nil
387+
388+
// Only requeue if not synced yet to avoid continuous reconciliation
389+
if !appSourcesSynced {
390+
return ctrl.Result{RequeueAfter: requeueTime}, nil
391+
}
392+
return ctrl.Result{}, nil
223393
}
224394

225395
func (r *Reconciler) reconcileArgoCDAppSet(ctx context.Context, resource *v1alpha1.CustomPackage, appSet *argov1alpha1.ApplicationSet) (ctrl.Result, error) {
@@ -270,7 +440,11 @@ func (r *Reconciler) reconcileArgoCDAppSet(ctx context.Context, resource *v1alph
270440

271441
resource.Status.Synced = resource.Status.Synced && gitGeneratorsSynced
272442

273-
return ctrl.Result{RequeueAfter: requeueTime}, nil
443+
// Only requeue if not synced yet to avoid continuous reconciliation
444+
if !resource.Status.Synced {
445+
return ctrl.Result{RequeueAfter: requeueTime}, nil
446+
}
447+
return ctrl.Result{}, nil
274448
}
275449

276450
// create a gitrepository custom resource, then let the git repository controller take care of the rest
@@ -285,6 +459,7 @@ func (r *Reconciler) reconcileArgoCDSource(ctx context.Context, resource *v1alph
285459
}
286460

287461
func (r *Reconciler) reconcileArgoCDSourceFromRemote(ctx context.Context, resource *v1alpha1.CustomPackage, appName, repoURL string) (ctrl.Result, *v1alpha1.GitRepository, error) {
462+
logger := log.FromContext(ctx)
288463
relativePath := strings.TrimPrefix(repoURL, v1alpha1.CNOEURIScheme)
289464
// no guarantee that this path exists
290465
dirPath := filepath.Join(resource.Spec.RemoteRepository.Path, relativePath)
@@ -296,9 +471,34 @@ func (r *Reconciler) reconcileArgoCDSourceFromRemote(ctx context.Context, resour
296471
},
297472
}
298473

474+
// Check if GitRepository already exists
475+
existingRepo := &v1alpha1.GitRepository{}
476+
getErr := r.Client.Get(ctx, client.ObjectKeyFromObject(repo), existingRepo)
477+
478+
if getErr == nil {
479+
// GitRepository exists - check if we should take it over
480+
shouldTakeOver, checkErr := r.shouldTakeOverGitRepository(ctx, resource, existingRepo)
481+
if checkErr != nil {
482+
return ctrl.Result{}, nil, fmt.Errorf("checking if should take over git repository: %w", checkErr)
483+
}
484+
485+
if !shouldTakeOver {
486+
// A higher/equal priority package owns this, just return it without updating
487+
logger.V(1).Info("Using existing GitRepository owned by higher priority package",
488+
"repo", repo.Name,
489+
"ourPriority", resource.ObjectMeta.Annotations[v1alpha1.PackagePriorityAnnotation])
490+
return ctrl.Result{}, existingRepo, nil
491+
}
492+
// We should take it over - proceed with CreateOrUpdate below
493+
} else if !errors.IsNotFound(getErr) {
494+
return ctrl.Result{}, nil, getErr
495+
}
496+
// GitRepository doesn't exist or we should take it over
497+
299498
cliStartTime, _ := util.GetCLIStartTimeAnnotationValue(resource.ObjectMeta.Annotations)
300499

301-
_, err := controllerutil.CreateOrUpdate(ctx, r.Client, repo, func() error {
500+
var err error
501+
_, err = controllerutil.CreateOrUpdate(ctx, r.Client, repo, func() error {
302502
if err := controllerutil.SetControllerReference(resource, repo, r.Scheme); err != nil {
303503
return err
304504
}
@@ -349,6 +549,30 @@ func (r *Reconciler) reconcileArgoCDSourceFromLocal(ctx context.Context, resourc
349549
},
350550
}
351551

552+
// Check if GitRepository already exists
553+
existingRepo := &v1alpha1.GitRepository{}
554+
getErr := r.Client.Get(ctx, client.ObjectKeyFromObject(repo), existingRepo)
555+
556+
if getErr == nil {
557+
// GitRepository exists - check if we should take it over
558+
shouldTakeOver, checkErr := r.shouldTakeOverGitRepository(ctx, resource, existingRepo)
559+
if checkErr != nil {
560+
return ctrl.Result{}, nil, fmt.Errorf("checking if should take over git repository: %w", checkErr)
561+
}
562+
563+
if !shouldTakeOver {
564+
// A higher/equal priority package owns this, just return it without updating
565+
logger.V(1).Info("Using existing GitRepository owned by higher priority package",
566+
"repo", repo.Name,
567+
"ourPriority", resource.ObjectMeta.Annotations[v1alpha1.PackagePriorityAnnotation])
568+
return ctrl.Result{}, existingRepo, nil
569+
}
570+
// We should take it over - proceed with CreateOrUpdate below
571+
} else if !errors.IsNotFound(getErr) {
572+
return ctrl.Result{}, nil, getErr
573+
}
574+
// GitRepository doesn't exist or we should take it over
575+
352576
cliStartTime, _ := util.GetCLIStartTimeAnnotationValue(resource.ObjectMeta.Annotations)
353577

354578
_, err = controllerutil.CreateOrUpdate(ctx, r.Client, repo, func() error {
@@ -469,7 +693,8 @@ func (r *Reconciler) reconcileHelmValueObjectSource(ctx context.Context,
469693
val[k] = v
470694
}
471695
}
472-
return ctrl.Result{RequeueAfter: requeueTime}, nil
696+
// No need to requeue for helm value processing
697+
return ctrl.Result{}, nil
473698
}
474699

475700
func localRepoName(appName, dir string) string {

0 commit comments

Comments
 (0)