@@ -37,6 +37,9 @@ var jobs = make(map[string]*cron.Cron)
3737var mtx sync.Mutex
3838var store storepkg.Store
3939
40+ // getUpdates is a package-level variable that can be replaced in tests for mocking
41+ var getUpdates = kotspull .GetUpdates
42+
4043// Start will start the update checker
4144// the frequency of those update checks are app specific and can be modified by the user
4245func Start () error {
@@ -251,7 +254,7 @@ func checkForKotsAppUpdates(opts types.CheckForUpdatesOpts, finishedChan chan<-
251254 }
252255
253256 // get updates
254- updates , err := kotspull . GetUpdates (fmt .Sprintf ("replicated://%s" , latestLicense .Spec .AppSlug ), getUpdatesOptions )
257+ updates , err := getUpdates (fmt .Sprintf ("replicated://%s" , latestLicense .Spec .AppSlug ), getUpdatesOptions )
255258 if err != nil {
256259 return nil , errors .Wrap (err , "failed to get updates" )
257260 }
@@ -274,7 +277,7 @@ func checkForKotsAppUpdates(opts types.CheckForUpdatesOpts, finishedChan chan<-
274277 return nil , errors .Errorf ("no app versions found for app %s in downstream %s" , opts .AppID , d .ClusterID )
275278 }
276279
277- if err := maybeUpdatePendingVersionsMetadata (a .ID , getUpdatesOptions , appVersions .CurrentVersion ); err != nil {
280+ if err := maybeUpdatePendingVersionsMetadata (a .ID , getUpdatesOptions , appVersions .CurrentVersion , appVersions . PendingVersions ); err != nil {
278281 logger .Error (errors .Wrap (err , "failed to update app version metadata" ))
279282 }
280283
@@ -337,13 +340,18 @@ func checkForKotsAppUpdates(opts types.CheckForUpdatesOpts, finishedChan chan<-
337340 return & ucr , nil
338341}
339342
343+ // getVersionKey builds a string given a chhanel ID and cursor to be used as a key for version lookup maps with the format <channelID>-<cursor>
344+ func getVersionKey (channelID , cursor string ) string {
345+ return fmt .Sprintf ("%s-%s" , channelID , cursor )
346+ }
347+
340348// maybeUpdatePendingVersionsMetadata updates metadata for pending versions since the currently deployed version.
341349//
342350// Limitations:
343351// - Only gets pending releases for the channel of the currently deployed version, even if channel changed in later versions
344352// - Does not rerender the application archive, so the Installation object in the archive can become out of sync
345353// - This is not in the critical path - errors are logged but don't fail the overall update check
346- func maybeUpdatePendingVersionsMetadata (appID string , getUpdatesOptions kotspull.GetUpdatesOptions , currentVersion * downstreamtypes.DownstreamVersion ) error {
354+ func maybeUpdatePendingVersionsMetadata (appID string , getUpdatesOptions kotspull.GetUpdatesOptions , currentVersion * downstreamtypes.DownstreamVersion , pendingVersions [] * downstreamtypes. DownstreamVersion ) error {
347355 if currentVersion == nil {
348356 return nil
349357 }
@@ -358,16 +366,51 @@ func maybeUpdatePendingVersionsMetadata(appID string, getUpdatesOptions kotspull
358366 getUpdatesOptions .CurrentChannelName = ""
359367 }
360368
361- updates , err := kotspull . GetUpdates (fmt .Sprintf ("replicated://%s" , getUpdatesOptions .License .Spec .AppSlug ), getUpdatesOptions )
369+ updates , err := getUpdates (fmt .Sprintf ("replicated://%s" , getUpdatesOptions .License .Spec .AppSlug ), getUpdatesOptions )
362370 if err != nil {
363371 return errors .Wrap (err , "get updates for metadata refresh" )
364372 }
365373
374+ // build a version map of the pending versions for fast lookup
375+ pendingVersionsMap := make (map [string ]* downstreamtypes.DownstreamVersion )
376+ for _ , pending := range pendingVersions {
377+ // We want to prevent mistakenly updating app versions by:
378+ // - only considering pending versions from the same channel as the currently deployed version
379+ // - making sure the cursor is different than the currently deployed version cursor, which can be the same when changing channels.
380+ if pending .ChannelID == currentVersion .ChannelID && pending .UpdateCursor != currentVersion .UpdateCursor {
381+ key := getVersionKey (pending .ChannelID , pending .UpdateCursor )
382+ pendingVersionsMap [key ] = pending
383+ }
384+ }
385+
386+ // build a version map of the updates for fast lookup
387+ updateVersionsMap := make (map [string ]* upstreamtypes.Update )
366388 for _ , update := range updates .Updates {
389+ // update the app version metadata with the upstream info
367390 if err := store .UpdateAppVersionMetadata (appID , update ); err != nil {
368391 logger .Error (errors .Wrapf (err , "failed to update app version metadata for %s" , update .VersionLabel ))
369392 }
393+ key := getVersionKey (update .ChannelID , update .Cursor )
394+ updateVersionsMap [key ] = & update
395+ }
396+
397+ // Determine versions to demote: pending but not in updates
398+ for key , pending := range pendingVersionsMap {
399+ if _ , exists := updateVersionsMap [key ]; ! exists {
400+ if err := store .UpdateAppVersionDemotion (appID , pending .ChannelID , pending .UpdateCursor , true ); err != nil {
401+ logger .Error (errors .Wrapf (err , "failed to update app version demotion state for %s" , pending .VersionLabel ))
402+ }
403+ }
370404 }
405+ // Determine versions to un-demote: updates but not in pending
406+ for key , update := range updateVersionsMap {
407+ if _ , exists := pendingVersionsMap [key ]; ! exists {
408+ if err := store .UpdateAppVersionDemotion (appID , update .ChannelID , update .Cursor , false ); err != nil {
409+ logger .Error (errors .Wrapf (err , "failed to update app version demotion state for %s" , update .VersionLabel ))
410+ }
411+ }
412+ }
413+
371414 return nil
372415}
373416
0 commit comments