@@ -31,6 +31,12 @@ const (
3131 CloudProfileAppliedConditionType string = "CloudProfileApplied"
3232)
3333
34+ // OCIFactory provides a hook for constructing OCI sources. It defaults to
35+ // cloudprofilesync.NewOCI but can be overridden in tests to simulate errors
36+ var OCIFactory = func (params cloudprofilesync.OCIParams , insecure bool ) (cloudprofilesync.Source , error ) {
37+ return cloudprofilesync .NewOCI (params , insecure )
38+ }
39+
3440type Reconciler struct {
3541 client.Client
3642}
@@ -53,6 +59,10 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
5359 cloudProfile .Spec = CloudProfileSpecToGardener (& mcp .Spec .CloudProfile )
5460 errs := make ([]error , 0 )
5561 for _ , updates := range mcp .Spec .MachineImageUpdates {
62+ // only call updater when an explicit source is provided
63+ if updates .Source .OCI == nil {
64+ continue
65+ }
5666 errs = append (errs , r .updateMachineImages (ctx , log , updates , & cloudProfile .Spec ))
5767 }
5868 gardenerv1beta1 .SetObjectDefaults_CloudProfile (& cloudProfile )
@@ -98,29 +108,48 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
98108 if err != nil {
99109 return ctrl.Result {}, err
100110 }
101- oci , err := cloudprofilesync . NewOCI (cloudprofilesync.OCIParams {
111+ src , err := OCIFactory (cloudprofilesync.OCIParams {
102112 Registry : updates .Source .OCI .Registry ,
103113 Repository : updates .Source .OCI .Repository ,
104114 Username : updates .Source .OCI .Username ,
105115 Password : string (password ),
106116 Parallel : 1 ,
107117 }, updates .Source .OCI .Insecure )
108118 if err != nil {
119+ if patchErr := r .patchStatusAndCondition (ctx , & mcp , v1alpha1 .FailedReconcileStatus , metav1.Condition {
120+ Type : CloudProfileAppliedConditionType ,
121+ Status : metav1 .ConditionFalse ,
122+ ObservedGeneration : mcp .Generation ,
123+ Reason : "GarbageCollectionFailed" ,
124+ Message : fmt .Sprintf ("failed to initialize OCI source for garbage collection: %s" , err ),
125+ }); patchErr != nil {
126+ return ctrl.Result {}, fmt .Errorf ("failed to patch ManagedCloudProfile status: %w (original error: %w)" , patchErr , err )
127+ }
109128 return ctrl.Result {}, fmt .Errorf ("failed to initialize OCI source for garbage collection: %w" , err )
110129 }
111- source = oci
130+ source = src
112131 default :
113132 continue
114133 }
115134
116135 versions , err := source .GetVersions (ctx )
117136 if err != nil {
137+ if patchErr := r .patchStatusAndCondition (ctx , & mcp , v1alpha1 .FailedReconcileStatus , metav1.Condition {
138+ Type : CloudProfileAppliedConditionType ,
139+ Status : metav1 .ConditionFalse ,
140+ ObservedGeneration : mcp .Generation ,
141+ Reason : "GarbageCollectionFailed" ,
142+ Message : fmt .Sprintf ("failed to list source versions for garbage collection: %s" , err ),
143+ }); patchErr != nil {
144+ return ctrl.Result {}, fmt .Errorf ("failed to patch ManagedCloudProfile status: %w (original error: %w)" , patchErr , err )
145+ }
118146 return ctrl.Result {}, fmt .Errorf ("failed to list source versions for garbage collection: %w" , err )
119147 }
120148
121149 referencedVersions := r .getReferencedVersions (ctx , mcp .Name , updates .ImageName )
122150
123151 cutoff := time .Now ().Add (- updates .GarbageCollection .MaxAge .Duration )
152+ versionsToDelete := make ([]string , 0 )
124153 for _ , v := range versions {
125154 if v .CreatedAt .IsZero () {
126155 continue
@@ -129,36 +158,56 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
129158 continue
130159 }
131160 if v .CreatedAt .Before (cutoff ) {
132- if err := r .deleteVersion (ctx , mcp .Name , updates .ImageName , v .Version ); err != nil {
133- if apierrors .IsInvalid (err ) {
134- log .V (1 ).Info ("garbage collection validation failed, skipping" , "image" , updates .ImageName , "version" , v .Version )
135- continue
136- }
137- return ctrl.Result {}, fmt .Errorf ("failed to delete image version: %w" , err )
161+ versionsToDelete = append (versionsToDelete , v .Version )
162+ }
163+ }
164+
165+ if len (versionsToDelete ) > 0 {
166+ if err := r .deleteVersion (ctx , mcp .Name , updates .ImageName , versionsToDelete ); err != nil {
167+ if apierrors .IsInvalid (err ) {
168+ log .V (1 ).Info ("garbage collection validation failed, skipping" , "image" , updates .ImageName )
169+ continue
138170 }
139- log .Info ("deleted image version from CloudProfile" , "image" , updates .ImageName , "version" , v .Version )
171+ if patchErr := r .patchStatusAndCondition (ctx , & mcp , v1alpha1 .FailedReconcileStatus , metav1.Condition {
172+ Type : CloudProfileAppliedConditionType ,
173+ Status : metav1 .ConditionFalse ,
174+ ObservedGeneration : mcp .Generation ,
175+ Reason : "GarbageCollectionFailed" ,
176+ Message : fmt .Sprintf ("failed to list source versions for garbage collection: %s" , err ),
177+ }); patchErr != nil {
178+ return ctrl.Result {}, fmt .Errorf ("failed to patch ManagedCloudProfile status: %w (original error: %w)" , patchErr , err )
179+ }
180+ return ctrl.Result {}, fmt .Errorf ("failed to delete image versions: %w" , err )
181+ }
182+ for _ , v := range versionsToDelete {
183+ log .Info ("deleted image version from CloudProfile" , "image" , updates .ImageName , "version" , v )
140184 }
141185 }
142186 }
143187
144188 return ctrl.Result {RequeueAfter : 5 * time .Minute }, nil
145189}
146190
147- func (r * Reconciler ) deleteVersion (ctx context.Context , cloudProfileName , imageName , version string ) error {
191+ func (r * Reconciler ) deleteVersion (ctx context.Context , cloudProfileName , imageName string , versions [] string ) error {
148192 var cp gardenerv1beta1.CloudProfile
149193 if err := r .Get (ctx , types.NamespacedName {Name : cloudProfileName }, & cp ); err != nil {
150194 return err
151195 }
196+
197+ versionsSet := make (map [string ]bool )
198+ for _ , v := range versions {
199+ versionsSet [v ] = true
200+ }
201+
152202 for i := range cp .Spec .MachineImages {
153203 if cp .Spec .MachineImages [i ].Name != imageName {
154204 continue
155205 }
156206 newVersions := make ([]gardenerv1beta1.MachineImageVersion , 0 , len (cp .Spec .MachineImages [i ].Versions ))
157207 for _ , mv := range cp .Spec .MachineImages [i ].Versions {
158- if mv .Version == version {
159- continue
208+ if ! versionsSet [ mv .Version ] {
209+ newVersions = append ( newVersions , mv )
160210 }
161- newVersions = append (newVersions , mv )
162211 }
163212 cp .Spec .MachineImages [i ].Versions = newVersions
164213 }
@@ -171,10 +220,16 @@ func (r *Reconciler) deleteVersion(ctx context.Context, cloudProfileName, imageN
171220 }
172221 filtered := make ([]providercfg.MachineImageVersion , 0 , len (cfg .MachineImages [i ].Versions ))
173222 for _ , mv := range cfg .MachineImages [i ].Versions {
174- if strings .HasSuffix (mv .Image , ":" + version ) {
175- continue
223+ found := false
224+ for _ , version := range versions {
225+ if strings .HasSuffix (mv .Image , ":" + version ) {
226+ found = true
227+ break
228+ }
229+ }
230+ if ! found {
231+ filtered = append (filtered , mv )
176232 }
177- filtered = append (filtered , mv )
178233 }
179234 cfg .MachineImages [i ].Versions = filtered
180235 }
@@ -191,22 +246,40 @@ func (r *Reconciler) deleteVersion(ctx context.Context, cloudProfileName, imageN
191246func (r * Reconciler ) getReferencedVersions (ctx context.Context , cloudProfileName , imageName string ) map [string ]bool {
192247 referenced := make (map [string ]bool )
193248
194- shootList := & gardenerv1beta1.ShootList {}
195- if err := r .List (ctx , shootList , client .InNamespace (metav1 .NamespaceAll )); err != nil {
196- return referenced
197- }
198-
199- for _ , shoot := range shootList .Items {
200- if shoot .Spec .CloudProfile == nil || shoot .Spec .CloudProfile .Name != cloudProfileName {
201- continue
249+ var cp gardenerv1beta1.CloudProfile
250+ if err := r .Get (ctx , types.NamespacedName {Name : cloudProfileName }, & cp ); err == nil {
251+ if cp .Spec .ProviderConfig != nil {
252+ var cfg providercfg.CloudProfileConfig
253+ if err := json .Unmarshal (cp .Spec .ProviderConfig .Raw , & cfg ); err == nil {
254+ for _ , img := range cfg .MachineImages {
255+ if img .Name != imageName {
256+ continue
257+ }
258+ for _ , v := range img .Versions {
259+ if idx := strings .LastIndex (v .Image , ":" ); idx != - 1 {
260+ version := v .Image [idx + 1 :]
261+ referenced [version ] = true
262+ }
263+ }
264+ }
265+ }
202266 }
267+ }
203268
204- for _ , worker := range shoot .Spec .Provider .Workers {
205- if worker .Machine .Image == nil || worker .Machine .Image .Name != imageName {
269+ shootList := & gardenerv1beta1.ShootList {}
270+ if err := r .List (ctx , shootList , client .InNamespace (metav1 .NamespaceAll )); err == nil {
271+ for _ , shoot := range shootList .Items {
272+ if shoot .Spec .CloudProfile == nil || shoot .Spec .CloudProfile .Name != cloudProfileName {
206273 continue
207274 }
208- if worker .Machine .Image .Version != nil {
209- referenced [* worker .Machine .Image .Version ] = true
275+
276+ for _ , worker := range shoot .Spec .Provider .Workers {
277+ if worker .Machine .Image == nil || worker .Machine .Image .Name != imageName {
278+ continue
279+ }
280+ if worker .Machine .Image .Version != nil {
281+ referenced [* worker .Machine .Image .Version ] = true
282+ }
210283 }
211284 }
212285 }
@@ -222,7 +295,7 @@ func (r *Reconciler) updateMachineImages(ctx context.Context, log logr.Logger, u
222295 if err != nil {
223296 return err
224297 }
225- oci , err := cloudprofilesync . NewOCI (cloudprofilesync.OCIParams {
298+ src , err := OCIFactory (cloudprofilesync.OCIParams {
226299 Registry : update .Source .OCI .Registry ,
227300 Repository : update .Source .OCI .Repository ,
228301 Username : update .Source .OCI .Username ,
@@ -232,7 +305,7 @@ func (r *Reconciler) updateMachineImages(ctx context.Context, log logr.Logger, u
232305 if err != nil {
233306 return fmt .Errorf ("failed to initialize oci source: %w" , err )
234307 }
235- source = oci
308+ source = src
236309 default :
237310 return errors .New ("no machine images source configured" )
238311 }
0 commit comments